/ GUNRPG.Infrastructure / Gossip / LedgerSyncEngine.cs
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  }