lunalib 1.7.3__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lunalib/.gitignore +3 -0
- lunalib/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/core/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/core/__pycache__/blockchain.cpython-310.pyc +0 -0
- lunalib/core/__pycache__/crypto.cpython-310.pyc +0 -0
- lunalib/core/__pycache__/mempool.cpython-310.pyc +0 -0
- lunalib/core/__pycache__/wallet.cpython-310.pyc +0 -0
- lunalib/core/blockchain.py +13 -16
- lunalib/core/mempool.py +1 -0
- lunalib/core/wallet.py +121 -24
- lunalib/core/wallet_db.py +48 -47
- lunalib/gtx/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/gtx/__pycache__/bill_registry.cpython-310.pyc +0 -0
- lunalib/gtx/__pycache__/digital_bill.cpython-310.pyc +0 -0
- lunalib/gtx/__pycache__/genesis.cpython-310.pyc +0 -0
- lunalib/mining/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/mining/__pycache__/cuda_manager.cpython-310.pyc +0 -0
- lunalib/mining/__pycache__/difficulty.cpython-310.pyc +0 -0
- lunalib/mining/__pycache__/miner.cpython-310.pyc +0 -0
- lunalib/storage/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/storage/__pycache__/cache.cpython-310.pyc +0 -0
- lunalib/storage/__pycache__/database.cpython-310.pyc +0 -0
- lunalib/storage/__pycache__/encryption.cpython-310.pyc +0 -0
- lunalib/tests/__pycache__/conftest.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_blockchain.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_crypto.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_gtx.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_mining.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_storage.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_transactions.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/__pycache__/test_wallet.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/conftest.py +41 -0
- lunalib/tests/init.py +0 -0
- lunalib/tests/integration/__pycache__/test_integration.cpython-310-pytest-9.0.1.pyc +0 -0
- lunalib/tests/integration/test_integration.py +62 -0
- lunalib/tests/test_blockchain.py +34 -0
- lunalib/tests/test_crypto.py +42 -0
- lunalib/tests/test_gtx.py +135 -0
- lunalib/tests/test_mining.py +244 -0
- lunalib/tests/test_security_suite.py +832 -0
- lunalib/tests/test_storage.py +84 -0
- lunalib/tests/test_transactions.py +103 -0
- lunalib/tests/test_wallet.py +91 -0
- lunalib/transactions/__pycache__/__init__.cpython-310.pyc +0 -0
- lunalib/transactions/__pycache__/security.cpython-310.pyc +0 -0
- lunalib/transactions/__pycache__/transactions.cpython-310.pyc +0 -0
- lunalib/transactions/__pycache__/validator.cpython-310.pyc +0 -0
- {lunalib-1.7.3.dist-info → lunalib-1.8.0.dist-info}/METADATA +1 -1
- lunalib-1.8.0.dist-info/RECORD +77 -0
- lunalib-1.7.3.dist-info/RECORD +0 -34
- {lunalib-1.7.3.dist-info → lunalib-1.8.0.dist-info}/WHEEL +0 -0
- {lunalib-1.7.3.dist-info → lunalib-1.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
def safe_print(*args, **kwargs):
|
|
3
|
+
encoding = sys.stdout.encoding or 'utf-8'
|
|
4
|
+
try:
|
|
5
|
+
print(*args, **kwargs)
|
|
6
|
+
except UnicodeEncodeError:
|
|
7
|
+
print(*(str(a).encode(encoding, errors='replace').decode(encoding) for a in args), **kwargs)
|
|
8
|
+
"""
|
|
9
|
+
Comprehensive Security and Integration Test Suite
|
|
10
|
+
|
|
11
|
+
Tests for:
|
|
12
|
+
- Reward-Difficulty Correlation
|
|
13
|
+
- Transaction Signature Verification
|
|
14
|
+
- Address Spoofing Prevention
|
|
15
|
+
- DDoS/Spam Protection
|
|
16
|
+
- Multi-wallet State Management
|
|
17
|
+
- Blockchain Integrity
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import unittest
|
|
21
|
+
import time
|
|
22
|
+
import hashlib
|
|
23
|
+
import threading
|
|
24
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
25
|
+
from typing import List, Dict
|
|
26
|
+
|
|
27
|
+
# Import components
|
|
28
|
+
from lunalib.core.wallet import LunaWallet
|
|
29
|
+
from lunalib.core.blockchain import BlockchainManager
|
|
30
|
+
from lunalib.core.mempool import MempoolManager
|
|
31
|
+
from lunalib.core.wallet_manager import WalletStateManager, Transaction, TransactionStatus
|
|
32
|
+
from lunalib.transactions.transactions import TransactionManager
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# TEST SUITE 1: REWARD-DIFFICULTY CORRELATION
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
class TestRewardDifficultyCorrelation(unittest.TestCase):
|
|
40
|
+
"""
|
|
41
|
+
Ensure rewards are correctly correlated to mining difficulty.
|
|
42
|
+
Difficulty N should ALWAYS result in N LKC reward.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def setUp(self):
|
|
46
|
+
"""Setup test fixtures"""
|
|
47
|
+
self.blockchain = BlockchainManager()
|
|
48
|
+
self.tx_manager = TransactionManager()
|
|
49
|
+
|
|
50
|
+
def test_difficulty_1_equals_1_lkc_reward(self):
|
|
51
|
+
"""Difficulty 1 should produce exactly 1 LKC reward"""
|
|
52
|
+
difficulty = 1
|
|
53
|
+
expected_reward = 1.0
|
|
54
|
+
|
|
55
|
+
# Simulate mined block with difficulty 1
|
|
56
|
+
block = self._create_test_block(difficulty=difficulty)
|
|
57
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
58
|
+
|
|
59
|
+
self.assertEqual(reward_tx['amount'], expected_reward,
|
|
60
|
+
f"Difficulty {difficulty} should reward {expected_reward} LKC")
|
|
61
|
+
|
|
62
|
+
def test_difficulty_2_equals_2_lkc_reward(self):
|
|
63
|
+
"""Difficulty 2 should produce exactly 2 LKC reward"""
|
|
64
|
+
difficulty = 2
|
|
65
|
+
expected_reward = 2.0
|
|
66
|
+
|
|
67
|
+
block = self._create_test_block(difficulty=difficulty)
|
|
68
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
69
|
+
|
|
70
|
+
self.assertEqual(reward_tx['amount'], expected_reward)
|
|
71
|
+
|
|
72
|
+
def test_difficulty_9_equals_9_lkc_reward(self):
|
|
73
|
+
"""Difficulty 9 should produce exactly 9 LKC reward"""
|
|
74
|
+
difficulty = 9
|
|
75
|
+
expected_reward = 9.0
|
|
76
|
+
|
|
77
|
+
block = self._create_test_block(difficulty=difficulty)
|
|
78
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
79
|
+
|
|
80
|
+
self.assertEqual(reward_tx['amount'], expected_reward)
|
|
81
|
+
|
|
82
|
+
def test_reward_scaling_linear(self):
|
|
83
|
+
"""Test that reward scales linearly with difficulty"""
|
|
84
|
+
rewards = {}
|
|
85
|
+
|
|
86
|
+
for difficulty in range(1, 10):
|
|
87
|
+
block = self._create_test_block(difficulty=difficulty)
|
|
88
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
89
|
+
rewards[difficulty] = reward_tx['amount']
|
|
90
|
+
|
|
91
|
+
# Verify linear relationship: reward = difficulty
|
|
92
|
+
for difficulty, reward in rewards.items():
|
|
93
|
+
self.assertEqual(reward, float(difficulty),
|
|
94
|
+
f"Reward for difficulty {difficulty} should be {difficulty}")
|
|
95
|
+
|
|
96
|
+
def test_reward_hash_verified(self):
|
|
97
|
+
"""Reward transaction must be cryptographically verified"""
|
|
98
|
+
block = self._create_test_block(difficulty=5)
|
|
99
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
100
|
+
|
|
101
|
+
# Verify signature
|
|
102
|
+
self.assertIn('signature', reward_tx)
|
|
103
|
+
self.assertNotEqual(reward_tx['signature'], 'unsigned',
|
|
104
|
+
"Reward must be signed")
|
|
105
|
+
|
|
106
|
+
# Verify it hasn't been tampered with
|
|
107
|
+
original_hash = reward_tx['hash']
|
|
108
|
+
self.assertEqual(len(original_hash), 64,
|
|
109
|
+
"Transaction hash must be valid SHA-256 (64 chars)")
|
|
110
|
+
|
|
111
|
+
def test_reward_tampering_detection(self):
|
|
112
|
+
"""Tampering with reward amount should be detectable"""
|
|
113
|
+
block = self._create_test_block(difficulty=5)
|
|
114
|
+
reward_tx = self._extract_reward_transaction(block)
|
|
115
|
+
|
|
116
|
+
original_hash = reward_tx['hash']
|
|
117
|
+
original_amount = reward_tx['amount']
|
|
118
|
+
|
|
119
|
+
# Tamper with amount
|
|
120
|
+
reward_tx['amount'] = 999.0
|
|
121
|
+
|
|
122
|
+
# Recalculate hash (what a tamperer would do)
|
|
123
|
+
tampered_hash = hashlib.sha256(
|
|
124
|
+
str({k: v for k, v in reward_tx.items() if k != 'hash'}).encode()
|
|
125
|
+
).hexdigest()
|
|
126
|
+
|
|
127
|
+
# Hashes should not match
|
|
128
|
+
self.assertNotEqual(tampered_hash, original_hash,
|
|
129
|
+
"Tampering should change transaction hash")
|
|
130
|
+
|
|
131
|
+
def test_reward_from_zero_difficulty_invalid(self):
|
|
132
|
+
"""Difficulty 0 or negative should produce no reward"""
|
|
133
|
+
for invalid_difficulty in [0, -1, -5]:
|
|
134
|
+
block = self._create_test_block(difficulty=invalid_difficulty)
|
|
135
|
+
reward_txs = [tx for tx in block.get('transactions', [])
|
|
136
|
+
if tx.get('type') == 'reward']
|
|
137
|
+
|
|
138
|
+
self.assertEqual(len(reward_txs), 0,
|
|
139
|
+
f"Difficulty {invalid_difficulty} should produce no reward")
|
|
140
|
+
|
|
141
|
+
def _create_test_block(self, difficulty: int = 1) -> Dict:
|
|
142
|
+
"""Create a test block with specified difficulty"""
|
|
143
|
+
return {
|
|
144
|
+
'index': 1,
|
|
145
|
+
'previous_hash': '0' * 64,
|
|
146
|
+
'timestamp': int(time.time()),
|
|
147
|
+
'transactions': [
|
|
148
|
+
{
|
|
149
|
+
'hash': hashlib.sha256(b'reward').hexdigest(),
|
|
150
|
+
'type': 'reward',
|
|
151
|
+
'from': 'network',
|
|
152
|
+
'to': 'LUN_MINER_ADDRESS',
|
|
153
|
+
'amount': float(difficulty),
|
|
154
|
+
'fee': 0.0,
|
|
155
|
+
'signature': 'network_signed',
|
|
156
|
+
'timestamp': int(time.time())
|
|
157
|
+
}
|
|
158
|
+
] if difficulty > 0 else [],
|
|
159
|
+
'difficulty': difficulty,
|
|
160
|
+
'nonce': 12345,
|
|
161
|
+
'hash': 'a' * difficulty + '0' * (64 - difficulty)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def _extract_reward_transaction(self, block: Dict) -> Dict:
|
|
165
|
+
"""Extract reward transaction from block"""
|
|
166
|
+
for tx in block.get('transactions', []):
|
|
167
|
+
if tx.get('type') == 'reward':
|
|
168
|
+
return tx
|
|
169
|
+
return {}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ============================================================================
|
|
173
|
+
# TEST SUITE 2: TRANSACTION SIGNATURE VERIFICATION
|
|
174
|
+
# ============================================================================
|
|
175
|
+
|
|
176
|
+
class TestTransactionSignatureVerification(unittest.TestCase):
|
|
177
|
+
"""
|
|
178
|
+
Ensure transactions cannot be forged - require valid cryptographic signatures.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def setUp(self):
|
|
182
|
+
"""Setup test fixtures"""
|
|
183
|
+
self.wallet = LunaWallet()
|
|
184
|
+
self.tx_manager = TransactionManager()
|
|
185
|
+
|
|
186
|
+
# Create test wallet
|
|
187
|
+
self.wallet.create_wallet("Test", "password123")
|
|
188
|
+
self.wallet.unlock_wallet(self.wallet.current_wallet_address, "password123")
|
|
189
|
+
|
|
190
|
+
def test_transaction_requires_valid_signature(self):
|
|
191
|
+
"""Transaction without valid signature should be rejected"""
|
|
192
|
+
invalid_tx = {
|
|
193
|
+
'hash': 'abc123',
|
|
194
|
+
'type': 'transfer',
|
|
195
|
+
'from': 'LUN_SENDER',
|
|
196
|
+
'to': 'LUN_RECEIVER',
|
|
197
|
+
'amount': 100.0,
|
|
198
|
+
'fee': 0.001,
|
|
199
|
+
'signature': 'invalid_signature', # Invalid!
|
|
200
|
+
'timestamp': int(time.time())
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Try to validate
|
|
204
|
+
is_valid, message = self.tx_manager.validate_transaction(invalid_tx)
|
|
205
|
+
|
|
206
|
+
self.assertFalse(is_valid or 'invalid' in message.lower(),
|
|
207
|
+
"Invalid signature should cause validation to fail")
|
|
208
|
+
|
|
209
|
+
def test_signed_transaction_has_valid_signature(self):
|
|
210
|
+
"""Properly signed transaction should have valid signature"""
|
|
211
|
+
tx = self.tx_manager.create_transaction(
|
|
212
|
+
from_address=self.wallet.address,
|
|
213
|
+
to_address='LUN_RECEIVER',
|
|
214
|
+
amount=100.0,
|
|
215
|
+
private_key=self.wallet.private_key,
|
|
216
|
+
transaction_type='transfer'
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Should have signature
|
|
220
|
+
self.assertIn('signature', tx)
|
|
221
|
+
self.assertNotEqual(tx['signature'], 'unsigned')
|
|
222
|
+
self.assertGreater(len(tx['signature']), 10)
|
|
223
|
+
|
|
224
|
+
def test_tampering_with_transaction_detects_invalid_signature(self):
|
|
225
|
+
"""Modifying transaction data should invalidate signature"""
|
|
226
|
+
tx = self.tx_manager.create_transaction(
|
|
227
|
+
from_address=self.wallet.address,
|
|
228
|
+
to_address='LUN_RECEIVER',
|
|
229
|
+
amount=100.0,
|
|
230
|
+
private_key=self.wallet.private_key,
|
|
231
|
+
transaction_type='transfer'
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
original_hash = tx['hash']
|
|
235
|
+
original_amount = tx['amount']
|
|
236
|
+
|
|
237
|
+
# Tamper with amount
|
|
238
|
+
tx['amount'] = 999.0
|
|
239
|
+
|
|
240
|
+
# Hash should change
|
|
241
|
+
new_hash = self.tx_manager._calculate_transaction_hash(tx)
|
|
242
|
+
|
|
243
|
+
self.assertNotEqual(new_hash, original_hash,
|
|
244
|
+
"Tampering with amount should change hash")
|
|
245
|
+
|
|
246
|
+
def test_wrong_private_key_produces_invalid_signature(self):
|
|
247
|
+
"""Using wrong private key should produce invalid signature"""
|
|
248
|
+
tx1 = self.tx_manager.create_transaction(
|
|
249
|
+
from_address=self.wallet.address,
|
|
250
|
+
to_address='LUN_RECEIVER',
|
|
251
|
+
amount=100.0,
|
|
252
|
+
private_key=self.wallet.private_key,
|
|
253
|
+
transaction_type='transfer'
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Create another wallet
|
|
257
|
+
self.wallet.create_new_wallet("Other", "pass")
|
|
258
|
+
other_address = self.wallet.current_wallet_address
|
|
259
|
+
|
|
260
|
+
# Create transaction with other key
|
|
261
|
+
tx2 = self.tx_manager.create_transaction(
|
|
262
|
+
from_address=other_address,
|
|
263
|
+
to_address='LUN_RECEIVER',
|
|
264
|
+
amount=100.0,
|
|
265
|
+
private_key=self.wallet.private_key,
|
|
266
|
+
transaction_type='transfer'
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Signatures should be different
|
|
270
|
+
self.assertNotEqual(tx1['signature'], tx2['signature'],
|
|
271
|
+
"Different private keys should produce different signatures")
|
|
272
|
+
|
|
273
|
+
def test_public_key_matches_private_key(self):
|
|
274
|
+
"""Public key derived from private key should match"""
|
|
275
|
+
from lunalib.core.crypto import KeyManager
|
|
276
|
+
|
|
277
|
+
key_manager = KeyManager()
|
|
278
|
+
private_key, public_key, address = key_manager.generate_keypair()
|
|
279
|
+
|
|
280
|
+
# Derived public key should match
|
|
281
|
+
derived_public = key_manager.derive_public_key(private_key)
|
|
282
|
+
|
|
283
|
+
self.assertEqual(derived_public, public_key,
|
|
284
|
+
"Derived public key must match generated public key")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ============================================================================
|
|
288
|
+
# TEST SUITE 3: ADDRESS SPOOFING PREVENTION
|
|
289
|
+
# ============================================================================
|
|
290
|
+
|
|
291
|
+
class TestAddressSpoofingPrevention(unittest.TestCase):
|
|
292
|
+
"""
|
|
293
|
+
Ensure addresses cannot be spoofed - from address must be verified.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def setUp(self):
|
|
297
|
+
"""Setup test fixtures"""
|
|
298
|
+
self.wallet = LunaWallet()
|
|
299
|
+
self.blockchain = BlockchainManager()
|
|
300
|
+
|
|
301
|
+
def test_address_format_validation(self):
|
|
302
|
+
"""Only properly formatted addresses should be accepted"""
|
|
303
|
+
valid_addresses = [
|
|
304
|
+
'LUN_abc123def456',
|
|
305
|
+
'LUN_' + 'a' * 30,
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
invalid_addresses = [
|
|
309
|
+
'INVALID_abc123',
|
|
310
|
+
'abc123', # Missing LUN_ prefix
|
|
311
|
+
'LUN', # Too short
|
|
312
|
+
'', # Empty
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
for addr in valid_addresses:
|
|
316
|
+
# Should normalize without error
|
|
317
|
+
normalized = self.blockchain._normalize_address(addr)
|
|
318
|
+
self.assertIsNotNone(normalized)
|
|
319
|
+
|
|
320
|
+
for addr in invalid_addresses:
|
|
321
|
+
# Should fail or normalize to empty
|
|
322
|
+
normalized = self.blockchain._normalize_address(addr)
|
|
323
|
+
# Either empty or clearly invalid
|
|
324
|
+
self.assertTrue(len(normalized) == 0 or 'invalid' in normalized.lower() or len(normalized) < 20)
|
|
325
|
+
|
|
326
|
+
def test_from_address_cannot_be_faked(self):
|
|
327
|
+
"""From address in transaction must match signing key"""
|
|
328
|
+
wallet1 = LunaWallet()
|
|
329
|
+
wallet1.create_wallet("Wallet1", "pass1")
|
|
330
|
+
wallet1.unlock_wallet(wallet1.current_wallet_address, "pass1")
|
|
331
|
+
|
|
332
|
+
wallet2 = LunaWallet()
|
|
333
|
+
wallet2.create_wallet("Wallet2", "pass2")
|
|
334
|
+
|
|
335
|
+
tx_manager = TransactionManager()
|
|
336
|
+
|
|
337
|
+
# Create transaction from wallet1
|
|
338
|
+
tx = tx_manager.create_transaction(
|
|
339
|
+
from_address=wallet1.address,
|
|
340
|
+
to_address='LUN_RECEIVER',
|
|
341
|
+
amount=100.0,
|
|
342
|
+
private_key=wallet1.private_key,
|
|
343
|
+
transaction_type='transfer'
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# The from_address should match wallet1
|
|
347
|
+
self.assertEqual(tx['from'], wallet1.address,
|
|
348
|
+
"From address must match the wallet creating it")
|
|
349
|
+
|
|
350
|
+
# Trying to change it should be detectable (hash mismatch)
|
|
351
|
+
original_hash = tx['hash']
|
|
352
|
+
tx['from'] = wallet2.address
|
|
353
|
+
new_hash = tx_manager._calculate_transaction_hash(tx)
|
|
354
|
+
|
|
355
|
+
self.assertNotEqual(original_hash, new_hash,
|
|
356
|
+
"Changing from address should invalidate transaction")
|
|
357
|
+
|
|
358
|
+
def test_address_case_sensitivity(self):
|
|
359
|
+
"""Addresses should be case-insensitive for comparison"""
|
|
360
|
+
addr1 = 'LUN_ABC123'
|
|
361
|
+
addr2 = 'lun_abc123'
|
|
362
|
+
|
|
363
|
+
norm1 = self.blockchain._normalize_address(addr1)
|
|
364
|
+
norm2 = self.blockchain._normalize_address(addr2)
|
|
365
|
+
|
|
366
|
+
self.assertEqual(norm1, norm2,
|
|
367
|
+
"Addresses should normalize to same value (case-insensitive)")
|
|
368
|
+
|
|
369
|
+
def test_address_prefix_cannot_be_omitted(self):
|
|
370
|
+
"""Addresses must have LUN_ prefix, cannot be spoofed without it"""
|
|
371
|
+
wallet = LunaWallet()
|
|
372
|
+
wallet.create_wallet("Test", "pass")
|
|
373
|
+
|
|
374
|
+
# Address should have prefix
|
|
375
|
+
self.assertTrue(wallet.address.startswith('LUN_') or
|
|
376
|
+
wallet.address.startswith('lun_'),
|
|
377
|
+
"Generated address must have LUN_ prefix")
|
|
378
|
+
|
|
379
|
+
def test_transaction_from_unregistered_address_rejected(self):
|
|
380
|
+
"""Transactions from unknown addresses should be tracked/rejected"""
|
|
381
|
+
blockchain = BlockchainManager()
|
|
382
|
+
mempool = MempoolManager()
|
|
383
|
+
|
|
384
|
+
# Transaction from unknown address
|
|
385
|
+
suspicious_tx = {
|
|
386
|
+
'hash': 'xyz789',
|
|
387
|
+
'type': 'transfer',
|
|
388
|
+
'from': 'LUN_UNKNOWN_SPOOFER',
|
|
389
|
+
'to': 'LUN_TARGET',
|
|
390
|
+
'amount': 1000000.0,
|
|
391
|
+
'fee': 0.0,
|
|
392
|
+
'signature': 'fake_signature',
|
|
393
|
+
'timestamp': int(time.time())
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Validation should fail
|
|
397
|
+
is_valid, msg = mempool._validate_transaction_basic(suspicious_tx)
|
|
398
|
+
|
|
399
|
+
# May pass basic validation but signature won't verify
|
|
400
|
+
# The key is that the blockchain won't credit the unregistered address
|
|
401
|
+
self.assertIn('from', suspicious_tx)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ============================================================================
|
|
405
|
+
# TEST SUITE 4: DDOS/SPAM PROTECTION
|
|
406
|
+
# ============================================================================
|
|
407
|
+
|
|
408
|
+
class TestDDoSSpamProtection(unittest.TestCase):
|
|
409
|
+
"""
|
|
410
|
+
Prevent DDoS, spam, and overwhelming requests.
|
|
411
|
+
Implement rate limiting, transaction throttling, block size limits.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
def setUp(self):
|
|
415
|
+
"""Setup test fixtures"""
|
|
416
|
+
self.mempool = MempoolManager()
|
|
417
|
+
self.blockchain = BlockchainManager()
|
|
418
|
+
|
|
419
|
+
def test_mempool_size_limit(self):
|
|
420
|
+
"""Mempool should have maximum size limit"""
|
|
421
|
+
# Check mempool has max size
|
|
422
|
+
self.assertGreater(self.mempool.max_mempool_size, 0,
|
|
423
|
+
"Mempool must have size limit")
|
|
424
|
+
self.assertEqual(self.mempool.max_mempool_size, 10000,
|
|
425
|
+
"Mempool max size should be reasonable (10000)")
|
|
426
|
+
|
|
427
|
+
def test_duplicate_transactions_rejected(self):
|
|
428
|
+
"""Duplicate transactions should not increase mempool"""
|
|
429
|
+
tx = {
|
|
430
|
+
'hash': 'duplicate_test_123',
|
|
431
|
+
'type': 'transfer',
|
|
432
|
+
'from': 'LUN_SENDER',
|
|
433
|
+
'to': 'LUN_RECEIVER',
|
|
434
|
+
'amount': 100.0,
|
|
435
|
+
'fee': 0.001,
|
|
436
|
+
'signature': 'sig',
|
|
437
|
+
'timestamp': int(time.time())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
initial_size = len(self.mempool.local_mempool)
|
|
441
|
+
|
|
442
|
+
# Add same transaction twice
|
|
443
|
+
self.mempool.add_transaction(tx)
|
|
444
|
+
size_after_first = len(self.mempool.local_mempool)
|
|
445
|
+
|
|
446
|
+
self.mempool.add_transaction(tx)
|
|
447
|
+
size_after_second = len(self.mempool.local_mempool)
|
|
448
|
+
|
|
449
|
+
# Should only be added once
|
|
450
|
+
self.assertEqual(size_after_first, size_after_second,
|
|
451
|
+
"Duplicate transactions should not be added again")
|
|
452
|
+
|
|
453
|
+
def test_transaction_rate_limiting(self):
|
|
454
|
+
"""Single sender should be rate-limited"""
|
|
455
|
+
sender = 'LUN_SPAMMER'
|
|
456
|
+
|
|
457
|
+
# Try to add many transactions from same sender
|
|
458
|
+
transactions_added = 0
|
|
459
|
+
|
|
460
|
+
for i in range(100):
|
|
461
|
+
tx = {
|
|
462
|
+
'hash': f'spam_tx_{i}',
|
|
463
|
+
'type': 'transfer',
|
|
464
|
+
'from': sender,
|
|
465
|
+
'to': f'LUN_RECEIVER_{i}',
|
|
466
|
+
'amount': 1.0,
|
|
467
|
+
'fee': 0.001,
|
|
468
|
+
'signature': 'sig',
|
|
469
|
+
'timestamp': int(time.time())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
success = self.mempool.add_transaction(tx)
|
|
473
|
+
if success:
|
|
474
|
+
transactions_added += 1
|
|
475
|
+
|
|
476
|
+
# Should accept some but not all (rate limiting)
|
|
477
|
+
# At minimum, should have reasonable limit
|
|
478
|
+
self.assertGreater(transactions_added, 0,
|
|
479
|
+
"Should accept some transactions")
|
|
480
|
+
self.assertLess(transactions_added, 100,
|
|
481
|
+
"Should rate-limit spam (not accept all 100)")
|
|
482
|
+
|
|
483
|
+
def test_minimum_fee_requirement(self):
|
|
484
|
+
"""Transactions should require minimum fee to prevent spam"""
|
|
485
|
+
tx_no_fee = {
|
|
486
|
+
'hash': 'no_fee_tx',
|
|
487
|
+
'type': 'transfer',
|
|
488
|
+
'from': 'LUN_SENDER',
|
|
489
|
+
'to': 'LUN_RECEIVER',
|
|
490
|
+
'amount': 100.0,
|
|
491
|
+
'fee': 0.0, # No fee!
|
|
492
|
+
'signature': 'sig',
|
|
493
|
+
'timestamp': int(time.time())
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
tx_with_fee = {
|
|
497
|
+
'hash': 'fee_tx',
|
|
498
|
+
'type': 'transfer',
|
|
499
|
+
'from': 'LUN_SENDER',
|
|
500
|
+
'to': 'LUN_RECEIVER',
|
|
501
|
+
'amount': 100.0,
|
|
502
|
+
'fee': 0.001, # Has fee
|
|
503
|
+
'signature': 'sig',
|
|
504
|
+
'timestamp': int(time.time())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# Fee transaction should be preferred
|
|
508
|
+
# (This is a design recommendation)
|
|
509
|
+
# Both may be added, but fee transaction should be prioritized
|
|
510
|
+
|
|
511
|
+
def test_block_size_limit(self):
|
|
512
|
+
"""Blocks should have maximum transaction size"""
|
|
513
|
+
# A reasonable block size might be 1MB or 10000 transactions
|
|
514
|
+
# This prevents blocks from being too large
|
|
515
|
+
|
|
516
|
+
# This is more relevant when submitting blocks
|
|
517
|
+
# A block with 1 million transactions should be rejected
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
def test_timestamp_validation(self):
|
|
521
|
+
"""Transactions too far in past/future should be rejected"""
|
|
522
|
+
now = int(time.time())
|
|
523
|
+
|
|
524
|
+
# Transaction from 24 hours in past
|
|
525
|
+
old_tx = {
|
|
526
|
+
'hash': 'old_tx',
|
|
527
|
+
'type': 'transfer',
|
|
528
|
+
'from': 'LUN_SENDER',
|
|
529
|
+
'to': 'LUN_RECEIVER',
|
|
530
|
+
'amount': 100.0,
|
|
531
|
+
'fee': 0.001,
|
|
532
|
+
'signature': 'sig',
|
|
533
|
+
'timestamp': now - 86400 # 24 hours ago
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Transaction from 5 minutes in future
|
|
537
|
+
future_tx = {
|
|
538
|
+
'hash': 'future_tx',
|
|
539
|
+
'type': 'transfer',
|
|
540
|
+
'from': 'LUN_SENDER',
|
|
541
|
+
'to': 'LUN_RECEIVER',
|
|
542
|
+
'amount': 100.0,
|
|
543
|
+
'fee': 0.001,
|
|
544
|
+
'signature': 'sig',
|
|
545
|
+
'timestamp': now + 300 # 5 minutes in future
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
# Validate old transaction
|
|
549
|
+
is_valid_old, msg_old = self.mempool._validate_transaction_basic(old_tx)
|
|
550
|
+
# This may be rejected or accepted depending on design
|
|
551
|
+
|
|
552
|
+
# Validate future transaction
|
|
553
|
+
is_valid_future, msg_future = self.mempool._validate_transaction_basic(future_tx)
|
|
554
|
+
|
|
555
|
+
# Future transactions should be rejected
|
|
556
|
+
self.assertFalse(is_valid_future,
|
|
557
|
+
"Transactions too far in future should be rejected")
|
|
558
|
+
|
|
559
|
+
def test_concurrent_transaction_handling(self):
|
|
560
|
+
"""System should handle concurrent transaction submissions safely"""
|
|
561
|
+
results = []
|
|
562
|
+
|
|
563
|
+
def submit_tx(tx_id):
|
|
564
|
+
tx = {
|
|
565
|
+
'hash': f'concurrent_tx_{tx_id}',
|
|
566
|
+
'type': 'transfer',
|
|
567
|
+
'from': 'LUN_SENDER',
|
|
568
|
+
'to': f'LUN_RECEIVER_{tx_id}',
|
|
569
|
+
'amount': 1.0,
|
|
570
|
+
'fee': 0.001,
|
|
571
|
+
'signature': 'sig',
|
|
572
|
+
'timestamp': int(time.time())
|
|
573
|
+
}
|
|
574
|
+
success = self.mempool.add_transaction(tx)
|
|
575
|
+
results.append(success)
|
|
576
|
+
|
|
577
|
+
# Submit 50 transactions concurrently
|
|
578
|
+
threads = [threading.Thread(target=submit_tx, args=(i,))
|
|
579
|
+
for i in range(50)]
|
|
580
|
+
|
|
581
|
+
for t in threads:
|
|
582
|
+
t.start()
|
|
583
|
+
|
|
584
|
+
for t in threads:
|
|
585
|
+
t.join()
|
|
586
|
+
|
|
587
|
+
# Should complete without deadlock
|
|
588
|
+
self.assertEqual(len(results), 50,
|
|
589
|
+
"All concurrent submissions should complete")
|
|
590
|
+
|
|
591
|
+
# Should have added most/all (depending on limits)
|
|
592
|
+
successful = sum(results)
|
|
593
|
+
self.assertGreater(successful, 0,
|
|
594
|
+
"Should successfully add some concurrent transactions")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# ============================================================================
|
|
598
|
+
# TEST SUITE 5: MULTI-WALLET STATE MANAGEMENT
|
|
599
|
+
# ============================================================================
|
|
600
|
+
|
|
601
|
+
class TestMultiWalletStateManagement(unittest.TestCase):
|
|
602
|
+
"""
|
|
603
|
+
Ensure wallet state is correctly maintained for multiple wallets.
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
def setUp(self):
|
|
607
|
+
"""Setup test fixtures"""
|
|
608
|
+
self.wallet = LunaWallet()
|
|
609
|
+
self.state_manager = WalletStateManager()
|
|
610
|
+
|
|
611
|
+
def test_multiple_wallets_register_correctly(self):
|
|
612
|
+
"""Multiple wallets should register and track separately"""
|
|
613
|
+
addresses = []
|
|
614
|
+
|
|
615
|
+
for i in range(5):
|
|
616
|
+
self.wallet.create_new_wallet(f"Wallet{i}", f"pass{i}")
|
|
617
|
+
addresses.append(self.wallet.current_wallet_address)
|
|
618
|
+
|
|
619
|
+
# Register with state manager
|
|
620
|
+
self.state_manager.register_wallets(addresses)
|
|
621
|
+
|
|
622
|
+
# Should have all 5 registered
|
|
623
|
+
self.assertEqual(len(self.state_manager.wallet_states), 5,
|
|
624
|
+
"Should register all 5 wallets")
|
|
625
|
+
|
|
626
|
+
def test_wallet_isolation(self):
|
|
627
|
+
"""One wallet's transactions should not affect another"""
|
|
628
|
+
wallet1 = LunaWallet()
|
|
629
|
+
wallet2 = LunaWallet()
|
|
630
|
+
|
|
631
|
+
wallet1.create_wallet("W1", "pass1")
|
|
632
|
+
wallet2.create_wallet("W2", "pass2")
|
|
633
|
+
|
|
634
|
+
state_manager = WalletStateManager()
|
|
635
|
+
state_manager.register_wallets([wallet1.address, wallet2.address])
|
|
636
|
+
|
|
637
|
+
# Create transactions for wallet1
|
|
638
|
+
blockchain_txs = {
|
|
639
|
+
wallet1.address: [
|
|
640
|
+
{
|
|
641
|
+
'hash': 'tx1',
|
|
642
|
+
'type': 'transfer',
|
|
643
|
+
'from': wallet1.address,
|
|
644
|
+
'to': 'LUN_OTHER',
|
|
645
|
+
'amount': 100.0,
|
|
646
|
+
'fee': 0.001,
|
|
647
|
+
'timestamp': int(time.time())
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
wallet2.address: [] # Wallet2 has no transactions
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
mempool_txs = {wallet1.address: [], wallet2.address: []}
|
|
654
|
+
|
|
655
|
+
# Sync
|
|
656
|
+
state_manager.sync_wallets_from_sources(blockchain_txs, mempool_txs)
|
|
657
|
+
|
|
658
|
+
# Wallet1 should have transaction
|
|
659
|
+
w1_state = state_manager.get_wallet_state(wallet1.address)
|
|
660
|
+
self.assertGreater(len(w1_state.confirmed_transactions), 0,
|
|
661
|
+
"Wallet1 should have transactions")
|
|
662
|
+
|
|
663
|
+
# Wallet2 should have none
|
|
664
|
+
w2_state = state_manager.get_wallet_state(wallet2.address)
|
|
665
|
+
self.assertEqual(len(w2_state.confirmed_transactions), 0,
|
|
666
|
+
"Wallet2 should have no transactions")
|
|
667
|
+
|
|
668
|
+
def test_balance_calculation_isolation(self):
|
|
669
|
+
"""Balances should be calculated independently per wallet"""
|
|
670
|
+
state_manager = WalletStateManager()
|
|
671
|
+
|
|
672
|
+
addr1 = 'LUN_ADDR1'
|
|
673
|
+
addr2 = 'LUN_ADDR2'
|
|
674
|
+
|
|
675
|
+
state_manager.register_wallets([addr1, addr2])
|
|
676
|
+
|
|
677
|
+
# Give different balances
|
|
678
|
+
blockchain_txs = {
|
|
679
|
+
addr1: [
|
|
680
|
+
{
|
|
681
|
+
'hash': 'tx1',
|
|
682
|
+
'type': 'reward',
|
|
683
|
+
'from': 'network',
|
|
684
|
+
'to': addr1,
|
|
685
|
+
'amount': 100.0,
|
|
686
|
+
'fee': 0.0,
|
|
687
|
+
'timestamp': int(time.time())
|
|
688
|
+
}
|
|
689
|
+
],
|
|
690
|
+
addr2: [
|
|
691
|
+
{
|
|
692
|
+
'hash': 'tx2',
|
|
693
|
+
'type': 'reward',
|
|
694
|
+
'from': 'network',
|
|
695
|
+
'to': addr2,
|
|
696
|
+
'amount': 50.0,
|
|
697
|
+
'fee': 0.0,
|
|
698
|
+
'timestamp': int(time.time())
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
state_manager.sync_wallets_from_sources(blockchain_txs, {})
|
|
704
|
+
|
|
705
|
+
# Check balances
|
|
706
|
+
bal1 = state_manager.get_balance(addr1)
|
|
707
|
+
bal2 = state_manager.get_balance(addr2)
|
|
708
|
+
|
|
709
|
+
self.assertEqual(bal1['confirmed_balance'], 100.0)
|
|
710
|
+
self.assertEqual(bal2['confirmed_balance'], 50.0,
|
|
711
|
+
"Different wallets should have independent balances")
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ============================================================================
|
|
715
|
+
# TEST SUITE 6: BLOCKCHAIN INTEGRITY
|
|
716
|
+
# ============================================================================
|
|
717
|
+
|
|
718
|
+
class TestBlockchainIntegrity(unittest.TestCase):
|
|
719
|
+
"""
|
|
720
|
+
Ensure blockchain cannot be modified or corrupted.
|
|
721
|
+
"""
|
|
722
|
+
|
|
723
|
+
def setUp(self):
|
|
724
|
+
"""Setup test fixtures"""
|
|
725
|
+
self.blockchain = BlockchainManager()
|
|
726
|
+
|
|
727
|
+
def test_block_hash_immutable(self):
|
|
728
|
+
"""Block hash should not change if block is valid"""
|
|
729
|
+
block_data = {
|
|
730
|
+
'index': 1,
|
|
731
|
+
'previous_hash': '0' * 64,
|
|
732
|
+
'timestamp': int(time.time()),
|
|
733
|
+
'transactions': [],
|
|
734
|
+
'miner': 'LUN_MINER',
|
|
735
|
+
'difficulty': 1,
|
|
736
|
+
'nonce': 12345,
|
|
737
|
+
'hash': 'a' + '0' * 63
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
original_hash = block_data['hash']
|
|
741
|
+
|
|
742
|
+
# Hash should not change
|
|
743
|
+
self.assertEqual(block_data['hash'], original_hash,
|
|
744
|
+
"Valid block hash should not change")
|
|
745
|
+
|
|
746
|
+
def test_block_modification_detectable(self):
|
|
747
|
+
"""Modifying block data should invalidate hash"""
|
|
748
|
+
block_data = {
|
|
749
|
+
'index': 1,
|
|
750
|
+
'previous_hash': '0' * 64,
|
|
751
|
+
'timestamp': int(time.time()),
|
|
752
|
+
'transactions': [
|
|
753
|
+
{
|
|
754
|
+
'hash': 'tx1',
|
|
755
|
+
'type': 'reward',
|
|
756
|
+
'amount': 5.0,
|
|
757
|
+
'to': 'LUN_MINER'
|
|
758
|
+
}
|
|
759
|
+
],
|
|
760
|
+
'miner': 'LUN_MINER',
|
|
761
|
+
'difficulty': 5,
|
|
762
|
+
'nonce': 12345,
|
|
763
|
+
'hash': 'aaaaa' + '0' * 59
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
# Calculate hash based on contents (excluding hash field)
|
|
767
|
+
block_contents = {k: v for k, v in block_data.items() if k != 'hash'}
|
|
768
|
+
content_str = str(block_contents)
|
|
769
|
+
calculated_hash = hashlib.sha256(content_str.encode()).hexdigest()
|
|
770
|
+
|
|
771
|
+
# Now tamper with transaction
|
|
772
|
+
block_data['transactions'][0]['amount'] = 999.0
|
|
773
|
+
|
|
774
|
+
# Recalculate
|
|
775
|
+
new_contents = {k: v for k, v in block_data.items() if k != 'hash'}
|
|
776
|
+
new_hash = hashlib.sha256(str(new_contents).encode()).hexdigest()
|
|
777
|
+
|
|
778
|
+
# Hashes should not match
|
|
779
|
+
self.assertNotEqual(calculated_hash, new_hash,
|
|
780
|
+
"Block modification should change hash")
|
|
781
|
+
|
|
782
|
+
def test_previous_block_reference_immutable(self):
|
|
783
|
+
"""Previous block hash reference should not change"""
|
|
784
|
+
block = {
|
|
785
|
+
'index': 2,
|
|
786
|
+
'previous_hash': 'abc123...' + '0' * 55,
|
|
787
|
+
'timestamp': int(time.time()),
|
|
788
|
+
'transactions': [],
|
|
789
|
+
'miner': 'LUN_MINER',
|
|
790
|
+
'difficulty': 1,
|
|
791
|
+
'nonce': 12345,
|
|
792
|
+
'hash': '0' * 64
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# Previous hash should be immutable
|
|
796
|
+
self.assertEqual(
|
|
797
|
+
block['previous_hash'],
|
|
798
|
+
'abc123...' + '0' * 55,
|
|
799
|
+
"Previous block reference must match"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# ============================================================================
|
|
804
|
+
# Run All Tests
|
|
805
|
+
# ============================================================================
|
|
806
|
+
|
|
807
|
+
if __name__ == '__main__':
|
|
808
|
+
# Create test suite
|
|
809
|
+
loader = unittest.TestLoader()
|
|
810
|
+
suite = unittest.TestSuite()
|
|
811
|
+
|
|
812
|
+
# Add all test suites
|
|
813
|
+
suite.addTests(loader.loadTestsFromTestCase(TestRewardDifficultyCorrelation))
|
|
814
|
+
suite.addTests(loader.loadTestsFromTestCase(TestTransactionSignatureVerification))
|
|
815
|
+
suite.addTests(loader.loadTestsFromTestCase(TestAddressSpoofingPrevention))
|
|
816
|
+
suite.addTests(loader.loadTestsFromTestCase(TestDDoSSpamProtection))
|
|
817
|
+
suite.addTests(loader.loadTestsFromTestCase(TestMultiWalletStateManagement))
|
|
818
|
+
suite.addTests(loader.loadTestsFromTestCase(TestBlockchainIntegrity))
|
|
819
|
+
|
|
820
|
+
# Run with verbose output
|
|
821
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
|
822
|
+
result = runner.run(suite)
|
|
823
|
+
|
|
824
|
+
# Print summary
|
|
825
|
+
safe_print("\n" + "="*70)
|
|
826
|
+
safe_print("COMPREHENSIVE TEST SUMMARY")
|
|
827
|
+
safe_print("="*70)
|
|
828
|
+
safe_print(f"Tests Run: {result.testsRun}")
|
|
829
|
+
safe_print(f"Failures: {len(result.failures)}")
|
|
830
|
+
safe_print(f"Errors: {len(result.errors)}")
|
|
831
|
+
safe_print(f"Success Rate: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%")
|
|
832
|
+
safe_print("="*70)
|