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 DistTestCore;
|
|
|
|
|
|
using GethPlugin;
|
2024-11-22 08:57:50 +01:00
|
|
|
|
using Nethereum.Hex.HexConvertors.Extensions;
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly Dictionary<TestLifecycle, MarketplaceHandle> handles = new Dictionary<TestLifecycle, MarketplaceHandle>();
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
protected override void LifecycleStart(TestLifecycle lifecycle)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.LifecycleStart(lifecycle);
|
2024-12-17 16:06:06 +01:00
|
|
|
|
var geth = StartGethNode(s => s.IsMiner());
|
2025-04-22 17:11:34 +02:00
|
|
|
|
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
|
2024-11-21 15:30:56 +01:00
|
|
|
|
handles.Add(lifecycle, new MarketplaceHandle(geth, contracts));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result)
|
|
|
|
|
|
{
|
|
|
|
|
|
handles.Remove(lifecycle);
|
2025-04-22 17:11:34 +02:00
|
|
|
|
base.LifecycleStop(lifecycle, result);
|
2024-11-21 15:30:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected IGethNode GetGeth()
|
|
|
|
|
|
{
|
|
|
|
|
|
return handles[Get()].Geth;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected ICodexContracts GetContracts()
|
|
|
|
|
|
{
|
|
|
|
|
|
return handles[Get()].Contracts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-17 15:54:52 +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; }
|
|
|
|
|
|
|
|
|
|
|
|
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-03-05 15:55:10 +01:00
|
|
|
|
public void AssertTstBalance(ICodexNode node, TestToken expectedBalance, string message)
|
2025-02-26 16:17:20 +01:00
|
|
|
|
{
|
2025-03-05 15:55:10 +01:00
|
|
|
|
AssertTstBalance(node.EthAddress, expectedBalance, message);
|
2025-02-26 16:17:20 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-05 15:55:10 +01:00
|
|
|
|
public void AssertTstBalance(EthAddress address, TestToken expectedBalance, string message)
|
2025-02-26 16:17:20 +01:00
|
|
|
|
{
|
|
|
|
|
|
var retry = GetBalanceAssertRetry();
|
|
|
|
|
|
retry.Run(() =>
|
|
|
|
|
|
{
|
2025-03-05 15:55:10 +01:00
|
|
|
|
var balance = GetTstBalance(address);
|
2025-02-26 16:17:20 +01:00
|
|
|
|
|
2025-03-05 15:55:10 +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);
|
|
|
|
|
|
|
2025-03-05 15:55:10 +01:00
|
|
|
|
if (balance != expectedBalance)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new Exception(nameof(AssertEthBalance) +
|
|
|
|
|
|
$" expected: {expectedBalance} but was: {balance} - message: " + message);
|
|
|
|
|
|
}
|
2025-02-26 16:17:20 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Retry GetBalanceAssertRetry()
|
|
|
|
|
|
{
|
|
|
|
|
|
return new Retry("AssertBalance",
|
2025-03-05 15:55:10 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-17 15:54:52 +01:00
|
|
|
|
public ICodexNode StartValidator()
|
|
|
|
|
|
{
|
|
|
|
|
|
return StartCodex(s => s
|
|
|
|
|
|
.WithName("validator")
|
|
|
|
|
|
.EnableMarketplace(GetGeth(), GetContracts(), m => m
|
|
|
|
|
|
.WithInitial(StartingBalanceEth.Eth(), StartingBalanceTST.Tst())
|
|
|
|
|
|
.AsValidator()
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-22 08:57:50 +01:00
|
|
|
|
public SlotFill[] GetOnChainSlotFills(ICodexNodeGroup possibleHosts, string purchaseId)
|
|
|
|
|
|
{
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public SlotFill[] GetOnChainSlotFills(ICodexNodeGroup possibleHosts)
|
|
|
|
|
|
{
|
|
|
|
|
|
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(() =>
|
|
|
|
|
|
{
|
2025-03-05 15:55:10 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2024-12-17 15:54:52 +01:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
|
|
|
|
|
Geth = geth;
|
|
|
|
|
|
Contracts = contracts;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IGethNode Geth { get; }
|
|
|
|
|
|
public ICodexContracts Contracts { get; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|