2025-05-29 16:37:20 +02:00
using CodexClient ;
2025-05-30 08:59:43 +02:00
using CodexReleaseTests.Utils ;
2025-05-29 16:37:20 +02:00
using Nethereum.Hex.HexConvertors.Extensions ;
using NUnit.Framework ;
using Utils ;
namespace CodexReleaseTests.MarketTests
{
[TestFixture]
public class RepairTest : MarketplaceAutoBootstrapDistTest
{
#region Setup
2025-06-02 13:32:03 +02:00
private readonly PurchaseParams purchaseParams = new PurchaseParams (
nodes : 4 ,
tolerance : 2 ,
uploadFilesize : 32. MB ( )
) ;
2025-05-29 16:37:20 +02:00
public RepairTest ( )
{
2025-06-02 13:32:03 +02:00
Assert . That ( purchaseParams . Nodes , Is . LessThan ( NumberOfHosts ) ) ;
2025-05-29 16:37:20 +02:00
}
protected override int NumberOfHosts = > 5 ;
protected override int NumberOfClients = > 1 ;
2025-06-02 13:32:03 +02:00
protected override ByteSize HostAvailabilitySize = > purchaseParams . SlotSize . Multiply ( 1.1 ) ; // Each host can hold 1 slot.
2025-05-29 17:06:44 +02:00
protected override TimeSpan HostAvailabilityMaxDuration = > TimeSpan . FromDays ( 5.0 ) ;
2025-05-29 16:37:20 +02:00
#endregion
2025-05-30 09:57:21 +02:00
[ Ignore ( "Test is ready. Waiting for repair implementation. " +
"Slots are never freed because proofs are never marked as missing. Issue: https://github.com/codex-storage/nim-codex/issues/1153" ) ]
2025-05-29 16:37:20 +02:00
[Test]
[Combinatorial]
public void RollingRepairSingleFailure (
2025-06-06 09:49:55 +02:00
[Rerun] int rerun ,
2025-05-29 16:37:20 +02:00
[Values(10)] int numFailures )
{
var hosts = StartHosts ( ) . ToList ( ) ;
var client = StartClients ( ) . Single ( ) ;
2025-05-30 08:59:43 +02:00
StartValidator ( ) ;
2025-05-29 16:37:20 +02:00
var contract = CreateStorageRequest ( client ) ;
contract . WaitForStorageContractStarted ( ) ;
// All slots are filled.
for ( var i = 0 ; i < numFailures ; i + + )
{
Log ( $"Failure step: {i}" ) ;
// Start a new host. Add it to the back of the list:
hosts . Add ( StartOneHost ( ) ) ;
var fill = GetSlotFillByOldestHost ( hosts ) ;
Log ( $"Causing failure for host: {fill.Host.GetName()} slotIndex: {fill.SlotFilledEvent.SlotIndex}" ) ;
hosts . Remove ( fill . Host ) ;
fill . Host . Stop ( waitTillStopped : true ) ;
// The slot should become free.
WaitForSlotFreedEvent ( contract , fill . SlotFilledEvent . SlotIndex ) ;
// One of the other hosts should pick up the free slot.
WaitForNewSlotFilledEvent ( contract , fill . SlotFilledEvent . SlotIndex ) ;
}
}
private void WaitForSlotFreedEvent ( IStoragePurchaseContract contract , ulong slotIndex )
{
Log ( nameof ( WaitForSlotFreedEvent ) ) ;
var start = DateTime . UtcNow ;
var timeout = CalculateContractFailTimespan ( ) ;
while ( DateTime . UtcNow < start + timeout )
{
var events = GetContracts ( ) . GetEvents ( GetTestRunTimeRange ( ) ) ;
var slotsFreed = events . GetSlotFreedEvents ( ) ;
Log ( $"Slots freed this period: {slotsFreed.Length}" ) ;
foreach ( var free in slotsFreed )
{
if ( free . RequestId . ToHex ( ) . ToLowerInvariant ( ) = = contract . PurchaseId . ToLowerInvariant ( ) )
{
if ( free . SlotIndex = = slotIndex )
{
Log ( "Found the correct slotFree event" ) ;
return ;
}
}
}
GetContracts ( ) . WaitUntilNextPeriod ( ) ;
}
Assert . Fail ( $"{nameof(WaitForSlotFreedEvent)} for contract {contract.PurchaseId} and slotIndex {slotIndex} failed after {Time.FormatDuration(timeout)}" ) ;
}
private void WaitForNewSlotFilledEvent ( IStoragePurchaseContract contract , ulong slotIndex )
{
Log ( nameof ( WaitForNewSlotFilledEvent ) ) ;
var start = DateTime . UtcNow ;
var timeout = contract . Purchase . Expiry ;
while ( DateTime . UtcNow < start + timeout )
{
var newTimeRange = new TimeRange ( start , DateTime . UtcNow ) ; // We only want to see new fill events.
var events = GetContracts ( ) . GetEvents ( newTimeRange ) ;
var slotFillEvents = events . GetSlotFilledEvents ( ) ;
var matches = slotFillEvents . Where ( f = >
{
return
f . RequestId . ToHex ( ) . ToLowerInvariant ( ) = = contract . PurchaseId . ToLowerInvariant ( ) & &
f . SlotIndex = = slotIndex ;
} ) . ToArray ( ) ;
if ( matches . Length > 1 )
{
var msg = string . Join ( "," , matches . Select ( f = > f . ToString ( ) ) ) ;
Assert . Fail ( $"Somehow, the slot got filled multiple times: {msg}" ) ;
}
if ( matches . Length = = 1 )
{
Log ( $"Found the correct new slotFilled event: {matches[0].ToString()}" ) ;
}
Thread . Sleep ( TimeSpan . FromSeconds ( 15 ) ) ;
}
Assert . Fail ( $"{nameof(WaitForSlotFreedEvent)} for contract {contract.PurchaseId} and slotIndex {slotIndex} failed after {Time.FormatDuration(timeout)}" ) ;
}
private SlotFill GetSlotFillByOldestHost ( List < ICodexNode > hosts )
{
var fills = GetOnChainSlotFills ( hosts ) ;
var copy = hosts . ToArray ( ) ;
foreach ( var host in copy )
{
var fill = GetFillByHost ( host , fills ) ;
if ( fill = = null )
{
// This host didn't fill anything.
// Move this one to the back of the list.
hosts . Remove ( host ) ;
hosts . Add ( host ) ;
}
else
{
return fill ;
}
}
throw new Exception ( "None of the hosts seem to have filled a slot." ) ;
}
private SlotFill ? GetFillByHost ( ICodexNode host , SlotFill [ ] fills )
{
// If these is more than 1 fill by this host, the test is misconfigured.
// The availability size of the host should guarantee it can fill 1 slot maximum.
return fills . SingleOrDefault ( f = > f . Host . EthAddress = = host . EthAddress ) ;
}
private IStoragePurchaseContract CreateStorageRequest ( ICodexNode client )
{
2025-06-02 13:32:03 +02:00
var cid = client . UploadFile ( GenerateTestFile ( purchaseParams . UploadFilesize ) ) ;
2025-05-29 16:37:20 +02:00
var config = GetContracts ( ) . Deployment . Config ;
return client . Marketplace . RequestStorage ( new StoragePurchaseRequest ( cid )
{
2025-05-29 17:06:44 +02:00
Duration = HostAvailabilityMaxDuration / 2 ,
2025-05-29 16:37:20 +02:00
Expiry = TimeSpan . FromMinutes ( 10.0 ) ,
2025-06-02 13:32:03 +02:00
MinRequiredNumberOfNodes = ( uint ) purchaseParams . Nodes ,
NodeFailureTolerance = ( uint ) purchaseParams . Tolerance ,
2025-05-29 16:37:20 +02:00
PricePerBytePerSecond = 10. TstWei ( ) ,
2025-05-30 08:59:43 +02:00
ProofProbability = 1 , // One proof every period. Free slot as quickly as possible.
2025-05-29 16:37:20 +02:00
CollateralPerByte = 1. TstWei ( )
} ) ;
}
}
}