436 lines
16 KiB
C#
Raw Normal View History

2025-01-16 15:13:16 +01:00
using CodexClient;
using CodexContractsPlugin;
2024-11-22 08:57:50 +01:00
using CodexContractsPlugin.Marketplace;
2024-11-21 15:30:56 +01:00
using CodexPlugin;
using CodexTests;
using GethPlugin;
using Logging;
2024-11-22 08:57:50 +01:00
using Nethereum.Hex.HexConvertors.Extensions;
2025-04-27 12:10:45 +02:00
using NUnit.Framework;
2024-11-22 16:09:18 +01:00
using Utils;
2024-11-21 15:30:56 +01:00
namespace CodexReleaseTests.MarketTests
{
public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest
{
2025-04-25 15:42:13 +02:00
private MarketplaceHandle handle = null!;
2024-11-21 15:30:56 +01:00
protected const int StartingBalanceTST = 1000;
2024-11-25 15:45:09 +01:00
protected const int StartingBalanceEth = 10;
2024-11-21 15:30:56 +01:00
2025-04-25 15:42:13 +02:00
[SetUp]
public void SetupMarketplace()
2024-11-21 15:30:56 +01:00
{
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
var monitor = SetupChainMonitor(GetTestLog(), contracts, GetTestRunTimeRange().From);
2025-04-29 12:14:06 +02:00
handle = new MarketplaceHandle(geth, contracts, monitor);
2024-11-21 15:30:56 +01:00
}
[TearDown]
public void TearDownMarketplace()
2024-11-21 15:30:56 +01:00
{
if (handle.ChainMonitor != null) handle.ChainMonitor.Stop();
2024-11-21 15:30:56 +01:00
}
protected IGethNode GetGeth()
{
2025-04-25 15:42:13 +02:00
return handle.Geth;
2024-11-21 15:30:56 +01:00
}
protected ICodexContracts GetContracts()
{
2025-04-25 15:42:13 +02:00
return handle.Contracts;
2024-11-21 15:30:56 +01:00
}
protected TimeSpan GetPeriodDuration()
{
var config = GetContracts().Deployment.Config;
return TimeSpan.FromSeconds(((double)config.Proofs.Period));
}
2024-11-22 16:09:18 +01:00
protected abstract int NumberOfHosts { get; }
protected abstract int NumberOfClients { get; }
protected abstract ByteSize HostAvailabilitySize { get; }
protected abstract TimeSpan HostAvailabilityMaxDuration { get; }
protected virtual bool MonitorChainState { get; } = true;
2024-11-22 16:09:18 +01:00
public ICodexNodeGroup StartHosts()
{
var hosts = StartCodex(NumberOfHosts, s => s
.WithName("host")
.EnableMarketplace(GetGeth(), GetContracts(), m => m
2024-11-25 15:45:09 +01:00
.WithInitial(StartingBalanceEth.Eth(), StartingBalanceTST.Tst())
2024-11-22 16:09:18 +01:00
.AsStorageNode()
)
);
var config = GetContracts().Deployment.Config;
foreach (var host in hosts)
{
2025-02-26 16:17:20 +01:00
AssertTstBalance(host, StartingBalanceTST.Tst(), nameof(StartHosts));
AssertEthBalance(host, StartingBalanceEth.Eth(), nameof(StartHosts));
2025-01-16 15:13:16 +01:00
host.Marketplace.MakeStorageAvailable(new StorageAvailability(
2024-11-22 16:09:18 +01:00
totalSpace: HostAvailabilitySize,
maxDuration: HostAvailabilityMaxDuration,
2025-01-25 14:07:15 +01:00
minPricePerBytePerSecond: 1.TstWei(),
totalCollateral: 999999.Tst())
2024-11-22 16:09:18 +01:00
);
}
return hosts;
}
2025-05-29 16:37:20 +02:00
public ICodexNode StartOneHost()
{
var host = StartCodex(s => s
.WithName("singlehost")
.EnableMarketplace(GetGeth(), GetContracts(), m => m
.WithInitial(StartingBalanceEth.Eth(), StartingBalanceTST.Tst())
.AsStorageNode()
)
);
var config = GetContracts().Deployment.Config;
AssertTstBalance(host, StartingBalanceTST.Tst(), nameof(StartOneHost));
AssertEthBalance(host, StartingBalanceEth.Eth(), nameof(StartOneHost));
host.Marketplace.MakeStorageAvailable(new StorageAvailability(
totalSpace: HostAvailabilitySize,
maxDuration: HostAvailabilityMaxDuration,
minPricePerBytePerSecond: 1.TstWei(),
totalCollateral: 999999.Tst())
);
return host;
}
public void AssertTstBalance(ICodexNode node, TestToken expectedBalance, string message)
2025-02-26 16:17:20 +01:00
{
AssertTstBalance(node.EthAddress, expectedBalance, message);
2025-02-26 16:17:20 +01:00
}
public void AssertTstBalance(EthAddress address, TestToken expectedBalance, string message)
2025-02-26 16:17:20 +01:00
{
var retry = GetBalanceAssertRetry();
retry.Run(() =>
{
var balance = GetTstBalance(address);
2025-02-26 16:17:20 +01:00
if (balance != expectedBalance)
{
throw new Exception(nameof(AssertTstBalance) +
$" expected: {expectedBalance} but was: {balance} - message: " + message);
}
2025-02-26 16:17:20 +01:00
});
}
public void AssertEthBalance(ICodexNode node, Ether expectedBalance, string message)
{
var retry = GetBalanceAssertRetry();
retry.Run(() =>
{
var balance = GetEthBalance(node);
if (balance != expectedBalance)
{
throw new Exception(nameof(AssertEthBalance) +
$" expected: {expectedBalance} but was: {balance} - message: " + message);
}
2025-02-26 16:17:20 +01:00
});
}
private ChainMonitor? SetupChainMonitor(ILog log, ICodexContracts contracts, DateTime startUtc)
{
if (!MonitorChainState) return null;
var result = new ChainMonitor(log, contracts, startUtc);
2025-04-27 12:10:45 +02:00
result.Start(() =>
{
log.Error("Failure in chain monitor. No chain updates after this point.");
//Assert.Fail("Failure in chain monitor.");
2025-04-27 12:10:45 +02:00
});
return result;
}
2025-02-26 16:17:20 +01:00
private Retry GetBalanceAssertRetry()
{
return new Retry("AssertBalance",
maxTimeout: TimeSpan.FromMinutes(10.0),
2025-02-26 16:17:20 +01:00
sleepAfterFail: TimeSpan.FromSeconds(10.0),
2025-04-08 13:07:55 +02:00
onFail: f => { },
failFast: false);
2025-02-26 16:17:20 +01:00
}
private TestToken GetTstBalance(ICodexNode node)
2024-11-25 15:45:09 +01:00
{
return GetContracts().GetTestTokenBalance(node);
}
2025-02-26 16:17:20 +01:00
private TestToken GetTstBalance(EthAddress address)
2024-11-25 15:45:09 +01:00
{
return GetContracts().GetTestTokenBalance(address);
}
2025-02-26 16:17:20 +01:00
private Ether GetEthBalance(ICodexNode node)
2024-11-25 15:45:09 +01:00
{
return GetGeth().GetEthBalance(node);
}
2025-02-26 16:17:20 +01:00
private Ether GetEthBalance(EthAddress address)
2024-11-25 15:45:09 +01:00
{
return GetGeth().GetEthBalance(address);
}
2024-11-22 16:09:18 +01:00
public ICodexNodeGroup StartClients()
{
2025-04-02 16:26:20 +02:00
return StartClients(s => { });
}
public ICodexNodeGroup StartClients(Action<ICodexSetup> additional)
{
return StartCodex(NumberOfClients, s =>
{
s.WithName("client")
.EnableMarketplace(GetGeth(), GetContracts(), m => m
.WithInitial(StartingBalanceEth.Eth(), StartingBalanceTST.Tst()));
additional(s);
});
2024-11-22 16:09:18 +01:00
}
public ICodexNode StartValidator()
{
return StartCodex(s => s
.WithName("validator")
.EnableMarketplace(GetGeth(), GetContracts(), m => m
.WithInitial(StartingBalanceEth.Eth(), StartingBalanceTST.Tst())
.AsValidator()
)
);
}
2025-05-29 16:37:20 +02:00
public SlotFill[] GetOnChainSlotFills(IEnumerable<ICodexNode> possibleHosts, string purchaseId)
2024-11-22 08:57:50 +01:00
{
2024-11-22 16:09:18 +01:00
var fills = GetOnChainSlotFills(possibleHosts);
return fills.Where(f => f
.SlotFilledEvent.RequestId.ToHex(false).ToLowerInvariant() == purchaseId.ToLowerInvariant())
2024-11-22 08:57:50 +01:00
.ToArray();
}
2025-05-29 16:37:20 +02:00
public SlotFill[] GetOnChainSlotFills(IEnumerable<ICodexNode> possibleHosts)
2024-11-22 08:57:50 +01:00
{
var events = GetContracts().GetEvents(GetTestRunTimeRange());
var fills = events.GetSlotFilledEvents();
return fills.Select(f =>
{
2024-11-22 16:09:18 +01:00
var host = possibleHosts.Single(h => h.EthAddress.Address == f.Host.Address);
2024-11-22 08:57:50 +01:00
return new SlotFill(f, host);
}).ToArray();
}
2025-02-04 13:52:30 +01:00
protected void AssertClientHasPaidForContract(TestToken pricePerBytePerSecond, ICodexNode client, IStoragePurchaseContract contract, ICodexNodeGroup hosts)
2024-11-25 16:10:17 +01:00
{
2025-02-04 13:52:30 +01:00
var expectedBalance = StartingBalanceTST.Tst() - GetContractFinalCost(pricePerBytePerSecond, contract, hosts);
2024-11-25 16:10:17 +01:00
2025-02-26 16:17:20 +01:00
AssertTstBalance(client, expectedBalance, "Client balance incorrect.");
2025-04-23 16:16:05 +02:00
Log($"Client has paid for contract. Balance: {expectedBalance}");
2024-11-25 16:10:17 +01:00
}
2025-02-04 13:52:30 +01:00
protected void AssertHostsWerePaidForContract(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts)
2024-11-25 16:10:17 +01:00
{
var fills = GetOnChainSlotFills(hosts);
var submitUtc = GetContractOnChainSubmittedUtc(contract);
var finishUtc = submitUtc + contract.Purchase.Duration;
2025-02-04 13:52:30 +01:00
var slotSize = Convert.ToInt64(contract.GetStatus().Request.Ask.SlotSize).Bytes();
2024-11-25 16:10:17 +01:00
var expectedBalances = new Dictionary<EthAddress, TestToken>();
2025-02-04 13:52:30 +01:00
2024-11-25 16:10:17 +01:00
foreach (var host in hosts) expectedBalances.Add(host.EthAddress, StartingBalanceTST.Tst());
foreach (var fill in fills)
{
var slotDuration = finishUtc - fill.SlotFilledEvent.Block.Utc;
2025-02-04 13:52:30 +01:00
expectedBalances[fill.Host.EthAddress] += GetContractCostPerSlot(pricePerBytePerSecond, slotSize, slotDuration);
2024-11-25 16:10:17 +01:00
}
2025-02-26 16:17:20 +01:00
foreach (var pair in expectedBalances)
2024-11-25 16:10:17 +01:00
{
2025-04-23 16:16:05 +02:00
AssertTstBalance(pair.Key, pair.Value, $"Host {pair.Key} was not paid for storage.");
Log($"Host {pair.Key} was paid for storage. Balance: {pair.Value}");
2025-02-26 16:17:20 +01:00
}
2024-11-25 16:10:17 +01:00
}
protected void AssertHostsCollateralsAreUnchanged(ICodexNodeGroup hosts)
{
// There is no separate collateral location yet.
// All host balances should be equal to or greater than the starting balance.
foreach (var host in hosts)
{
2025-02-26 16:17:20 +01:00
var retry = GetBalanceAssertRetry();
retry.Run(() =>
{
if (GetTstBalance(host) < StartingBalanceTST.Tst())
{
throw new Exception(nameof(AssertHostsCollateralsAreUnchanged));
}
2025-02-26 16:17:20 +01:00
});
2024-11-25 16:10:17 +01:00
}
}
protected void WaitForContractStarted(IStoragePurchaseContract r)
{
try
{
r.WaitForStorageContractStarted();
}
catch
{
// Contract failed to start. Retrieve and log every call to ReserveSlot to identify which hosts
// should have filled the slot.
var requestId = r.PurchaseId.ToLowerInvariant();
2025-05-22 13:55:03 +02:00
var calls = new List<ReserveSlotFunction>();
GetContracts().GetEvents(GetTestRunTimeRange()).GetReserveSlotCalls(calls.Add);
2025-05-22 13:55:03 +02:00
Log($"Request '{requestId}' failed to start. There were {calls.Count} hosts who called reserve-slot for it:");
foreach (var c in calls)
{
2025-05-20 10:19:07 +02:00
Log($" - {c.Block.Utc} Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}");
}
throw;
}
}
2025-02-04 13:52:30 +01:00
private TestToken GetContractFinalCost(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts)
2024-11-25 16:10:17 +01:00
{
var fills = GetOnChainSlotFills(hosts);
var result = 0.Tst();
var submitUtc = GetContractOnChainSubmittedUtc(contract);
var finishUtc = submitUtc + contract.Purchase.Duration;
2025-02-04 13:52:30 +01:00
var slotSize = Convert.ToInt64(contract.GetStatus().Request.Ask.SlotSize).Bytes();
2024-11-25 16:10:17 +01:00
foreach (var fill in fills)
{
var slotDuration = finishUtc - fill.SlotFilledEvent.Block.Utc;
2025-02-04 13:52:30 +01:00
result += GetContractCostPerSlot(pricePerBytePerSecond, slotSize, slotDuration);
2024-11-25 16:10:17 +01:00
}
return result;
}
private DateTime GetContractOnChainSubmittedUtc(IStoragePurchaseContract contract)
{
return Time.Retry<DateTime>(() =>
{
var events = GetContracts().GetEvents(GetTestRunTimeRange());
var submitEvent = events.GetStorageRequests().SingleOrDefault(e => e.RequestId.ToHex(false) == contract.PurchaseId);
if (submitEvent == null)
{
// We're too early.
throw new TimeoutException(nameof(GetContractOnChainSubmittedUtc) + "StorageRequest not found on-chain.");
}
return submitEvent.Block.Utc;
}, nameof(GetContractOnChainSubmittedUtc));
2024-11-25 16:10:17 +01:00
}
2025-02-04 13:52:30 +01:00
private TestToken GetContractCostPerSlot(TestToken pricePerBytePerSecond, ByteSize slotSize, TimeSpan slotDuration)
2024-11-25 16:10:17 +01:00
{
2025-02-04 13:52:30 +01:00
var cost = pricePerBytePerSecond.TstWei * slotSize.SizeInBytes * (int)slotDuration.TotalSeconds;
return cost.TstWei();
2024-11-25 16:10:17 +01:00
}
protected void AssertContractSlotsAreFilledByHosts(IStoragePurchaseContract contract, ICodexNodeGroup hosts)
{
var activeHosts = new Dictionary<int, SlotFill>();
Time.Retry(() =>
{
var fills = GetOnChainSlotFills(hosts, contract.PurchaseId);
foreach (var fill in fills)
{
var index = (int)fill.SlotFilledEvent.SlotIndex;
if (!activeHosts.ContainsKey(index))
{
activeHosts.Add(index, fill);
}
}
if (activeHosts.Count != contract.Purchase.MinRequiredNumberOfNodes) throw new Exception("Not all slots were filled...");
}, nameof(AssertContractSlotsAreFilledByHosts));
}
protected void AssertContractIsOnChain(IStoragePurchaseContract contract)
{
AssertOnChainEvents(events =>
{
var onChainRequests = events.GetStorageRequests();
if (onChainRequests.Any(r => r.Id == contract.PurchaseId)) return;
throw new Exception($"OnChain request {contract.PurchaseId} not found...");
}, nameof(AssertContractIsOnChain));
}
protected void AssertOnChainEvents(Action<ICodexContractsEvents> onEvents, string description)
{
Time.Retry(() =>
{
var events = GetContracts().GetEvents(GetTestRunTimeRange());
onEvents(events);
}, description);
}
2025-05-29 16:37:20 +02:00
protected TimeSpan CalculateContractFailTimespan()
{
var config = GetContracts().Deployment.Config;
var requiredNumMissedProofs = Convert.ToInt32(config.Collateral.MaxNumberOfSlashes);
var periodDuration = GetPeriodDuration();
var gracePeriod = periodDuration;
// Each host could miss 1 proof per period,
// so the time we should wait is period time * requiredNum of missed proofs.
// Except: the proof requirement has a concept of "downtime":
// a segment of time where proof is not required.
// We calculate the probability of downtime and extend the waiting
// timeframe by a factor, such that all hosts are highly likely to have
// failed a sufficient number of proofs.
float n = requiredNumMissedProofs;
return gracePeriod + (periodDuration * n * GetDowntimeFactor(config));
}
private float GetDowntimeFactor(MarketplaceConfig config)
{
byte numBlocksInDowntimeSegment = config.Proofs.Downtime;
float downtime = numBlocksInDowntimeSegment;
float window = 256.0f;
var chanceOfDowntime = downtime / window;
return 1.0f + chanceOfDowntime + chanceOfDowntime;
}
2024-11-22 08:57:50 +01:00
public class SlotFill
{
public SlotFill(SlotFilledEventDTO slotFilledEvent, ICodexNode host)
{
SlotFilledEvent = slotFilledEvent;
Host = host;
}
public SlotFilledEventDTO SlotFilledEvent { get; }
public ICodexNode Host { get; }
}
2024-11-21 15:30:56 +01:00
private class MarketplaceHandle
{
public MarketplaceHandle(IGethNode geth, ICodexContracts contracts, ChainMonitor? chainMonitor)
2024-11-21 15:30:56 +01:00
{
Geth = geth;
Contracts = contracts;
ChainMonitor = chainMonitor;
2024-11-21 15:30:56 +01:00
}
public IGethNode Geth { get; }
public ICodexContracts Contracts { get; }
public ChainMonitor? ChainMonitor { get; }
2024-11-21 15:30:56 +01:00
}
}
}