LedgerSyncEngine.cs
1 using System.Security.Cryptography; 2 using GUNRPG.Ledger; 3 using GUNRPG.Ledger.Indexing; 4 5 namespace GUNRPG.Gossip; 6 7 public class LedgerSyncEngine 8 { 9 private const int HashSize = SHA256.HashSizeInBytes; 10 11 public const int MaxSyncBatchSize = 256; 12 13 private readonly RunLedger _ledger; 14 15 public LedgerSyncEngine(RunLedger ledger) 16 { 17 ArgumentNullException.ThrowIfNull(ledger); 18 _ledger = ledger; 19 } 20 21 public bool NeedsSync(LedgerHead peerHead) 22 { 23 ArgumentNullException.ThrowIfNull(peerHead); 24 25 if (peerHead.EntryHash.Length != HashSize) 26 { 27 return false; 28 } 29 30 var localHead = _ledger.GetHead(); 31 32 if (peerHead.Index == localHead.Index && 33 !CryptographicOperations.FixedTimeEquals(peerHead.EntryHash.AsSpan(), localHead.EntryHash.AsSpan())) 34 { 35 // Fork detected — same index but diverging hashes; reject sync 36 return false; 37 } 38 39 return peerHead.Index > localHead.Index; 40 } 41 42 public bool IsSameHead(LedgerHead peerHead) 43 { 44 ArgumentNullException.ThrowIfNull(peerHead); 45 46 if (peerHead.EntryHash.Length != HashSize) 47 { 48 return false; 49 } 50 51 var localHead = _ledger.GetHead(); 52 return peerHead.Index == localHead.Index && 53 CryptographicOperations.FixedTimeEquals(peerHead.EntryHash.AsSpan(), localHead.EntryHash.AsSpan()); 54 } 55 56 public LedgerSyncRequest BuildSyncRequest(LedgerHead peerHead) 57 { 58 ArgumentNullException.ThrowIfNull(peerHead); 59 60 if (!NeedsSync(peerHead)) 61 { 62 throw new InvalidOperationException( 63 "Cannot build a sync request: NeedsSync(peerHead) must return true before calling BuildSyncRequest."); 64 } 65 66 var localHead = _ledger.GetHead(); 67 return new LedgerSyncRequest(localHead.Index + 1); 68 } 69 70 public bool ApplyResponse(LedgerSyncResponse response) 71 { 72 ArgumentNullException.ThrowIfNull(response); 73 74 if (response.Entries.Count > MaxSyncBatchSize) 75 { 76 return false; 77 } 78 79 foreach (var entry in response.Entries) 80 { 81 if (!_ledger.TryAppendEntry(entry)) 82 { 83 return false; 84 } 85 } 86 87 return true; 88 } 89 90 public bool ResolveFork(IReadOnlyList<RunLedgerEntry> peerEntries) 91 { 92 ArgumentNullException.ThrowIfNull(peerEntries); 93 94 if (!RunLedger.VerifyEntries(peerEntries)) 95 { 96 return false; 97 } 98 99 if (peerEntries.Count == 0) 100 { 101 return false; 102 } 103 104 var localHead = _ledger.GetHead(); 105 var peerHead = new LedgerHead(peerEntries[^1].Index, peerEntries[^1].EntryHash); 106 107 if (peerHead.Index <= localHead.Index) 108 { 109 return false; 110 } 111 112 var peerIndex = new MerkleSkipIndex(peerEntries); 113 var divergenceIndex = _ledger.MerkleSkipIndex.FindDivergenceIndex(peerIndex); 114 115 if (divergenceIndex < 0) 116 { 117 divergenceIndex = _ledger.Entries.Count; 118 } 119 120 if (divergenceIndex > peerEntries.Count) 121 { 122 return false; 123 } 124 125 for (var i = 0; i < divergenceIndex; i++) 126 { 127 if (!CryptographicOperations.FixedTimeEquals( 128 _ledger.Entries[i].EntryHash.AsSpan(), 129 peerEntries[i].EntryHash.AsSpan())) 130 { 131 return false; 132 } 133 } 134 135 _ledger.ReplaceEntriesFrom(divergenceIndex, peerEntries, (int)divergenceIndex); 136 return true; 137 } 138 }