From 6e45c638e7fc56779068da743d173bfac8bf0e09 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 22 Apr 2025 17:11:34 +0200 Subject: [PATCH 01/69] Switches from docker image label to debug-info version field --- ProjectPlugins/CodexClient/CodexTypes.cs | 1 + ProjectPlugins/CodexClient/Mapper.cs | 5 +- .../CodexContractsContainerRecipe.cs | 16 ++- .../CodexContractsPlugin.cs | 17 +-- .../CodexContractsPlugin.csproj | 1 + .../CodexContractsStarter.cs | 12 +- .../CoreInterfaceExtensions.cs | 16 +-- .../CodexContractsPlugin/VersionRegistry.cs | 125 ------------------ .../CodexPlugin/CodexDockerImage.cs | 6 +- ProjectPlugins/CodexPlugin/CodexPlugin.cs | 1 - .../DataTests/DataExpiryTest.cs | 3 +- .../MarketplaceAutoBootstrapDistTest.cs | 5 +- .../AutoBootstrapDistTest.cs | 21 +-- .../BasicTests/MarketplaceTests.cs | 2 +- .../FullyConnectedDownloadTests.cs | 2 +- .../PeerDiscoveryTests/PeerDiscoveryTests.cs | 2 +- Tools/CodexNetDeployer/Deployer.cs | 6 +- 17 files changed, 60 insertions(+), 181 deletions(-) delete mode 100644 ProjectPlugins/CodexContractsPlugin/VersionRegistry.cs diff --git a/ProjectPlugins/CodexClient/CodexTypes.cs b/ProjectPlugins/CodexClient/CodexTypes.cs index 9085aa2c..f9a0d9d2 100644 --- a/ProjectPlugins/CodexClient/CodexTypes.cs +++ b/ProjectPlugins/CodexClient/CodexTypes.cs @@ -17,6 +17,7 @@ namespace CodexClient { public string Version { get; set; } = string.Empty; public string Revision { get; set; } = string.Empty; + public string Contracts { get; set; } = string.Empty; public bool IsValid() { diff --git a/ProjectPlugins/CodexClient/Mapper.cs b/ProjectPlugins/CodexClient/Mapper.cs index 43bb15d0..47b1215c 100644 --- a/ProjectPlugins/CodexClient/Mapper.cs +++ b/ProjectPlugins/CodexClient/Mapper.cs @@ -165,10 +165,13 @@ namespace CodexClient private DebugInfoVersion Map(CodexOpenApi.CodexVersion obj) { + throw new Exception("waiting for new codex image with contracts revision"); + return new DebugInfoVersion { Version = obj.Version, - Revision = obj.Revision + Revision = obj.Revision, + Contracts = "aaa" }; } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs index e09a0ee7..b46f3c96 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs @@ -1,4 +1,5 @@ -using GethPlugin; +using CodexClient; +using GethPlugin; using KubernetesWorkflow; using KubernetesWorkflow.Recipe; @@ -8,14 +9,14 @@ namespace CodexContractsPlugin { public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json"; public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json"; - private readonly VersionRegistry versionRegistry; + private readonly DebugInfoVersion versionInfo; public override string AppName => "codex-contracts"; - public override string Image => versionRegistry.GetContractsDockerImage(); + public override string Image => GetContractsDockerImage(); - public CodexContractsContainerRecipe(VersionRegistry versionRegistry) + public CodexContractsContainerRecipe(DebugInfoVersion versionInfo) { - this.versionRegistry = versionRegistry; + this.versionInfo = versionInfo; } protected override void Initialize(StartupConfig startupConfig) @@ -30,5 +31,10 @@ namespace CodexContractsPlugin AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork"); AddEnvVar("KEEP_ALIVE", "1"); } + + private string GetContractsDockerImage() + { + return $"codexstorage/codex-contracts-eth:sha-{versionInfo.Contracts}-dist-tests"; + } } } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.cs index 6e02280d..1d122d3b 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.cs @@ -7,15 +7,11 @@ namespace CodexContractsPlugin { private readonly IPluginTools tools; private readonly CodexContractsStarter starter; - private readonly VersionRegistry versionRegistry; - private readonly CodexContractsContainerRecipe recipe; public CodexContractsPlugin(IPluginTools tools) { this.tools = tools; - versionRegistry = new VersionRegistry(tools.GetLog()); - recipe = new CodexContractsContainerRecipe(versionRegistry); - starter = new CodexContractsStarter(tools, recipe); + starter = new CodexContractsStarter(tools); } public string LogPrefix => "(CodexContracts) "; @@ -31,16 +27,16 @@ namespace CodexContractsPlugin public void AddMetadata(IAddMetadata metadata) { - metadata.Add("codexcontractsid", recipe.Image); + metadata.Add("codexcontractsid", "dynamic"); } public void Decommission() { } - public CodexContractsDeployment DeployContracts(CoreInterface ci, IGethNode gethNode) + public CodexContractsDeployment DeployContracts(CoreInterface ci, IGethNode gethNode, CodexClient.DebugInfoVersion versionInfo) { - return starter.Deploy(ci, gethNode); + return starter.Deploy(ci, gethNode, versionInfo); } public ICodexContracts WrapDeploy(IGethNode gethNode, CodexContractsDeployment deployment) @@ -48,10 +44,5 @@ namespace CodexContractsPlugin deployment = SerializeGate.Gate(deployment); return starter.Wrap(gethNode, deployment); } - - public void SetCodexDockerImageProvider(ICodexDockerImageProvider provider) - { - versionRegistry.SetProvider(provider); - } } } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.csproj b/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.csproj index 24f87068..34f6cad1 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.csproj +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsPlugin.csproj @@ -13,6 +13,7 @@ + diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs index 5ecc22a9..c7e900be 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs @@ -1,4 +1,5 @@ -using CodexContractsPlugin.Marketplace; +using CodexClient; +using CodexContractsPlugin.Marketplace; using Core; using GethPlugin; using KubernetesWorkflow; @@ -12,15 +13,13 @@ namespace CodexContractsPlugin public class CodexContractsStarter { private readonly IPluginTools tools; - private readonly CodexContractsContainerRecipe recipe; - public CodexContractsStarter(IPluginTools tools, CodexContractsContainerRecipe recipe) + public CodexContractsStarter(IPluginTools tools) { this.tools = tools; - this.recipe = recipe; } - public CodexContractsDeployment Deploy(CoreInterface ci, IGethNode gethNode) + public CodexContractsDeployment Deploy(CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo) { Log("Starting Codex SmartContracts container..."); @@ -28,6 +27,9 @@ namespace CodexContractsPlugin var startupConfig = CreateStartupConfig(gethNode); startupConfig.NameOverride = "codex-contracts"; + var recipe = new CodexContractsContainerRecipe(versionInfo); + Log($"Using image: {recipe.Image}"); + var containers = workflow.Start(1, recipe, startupConfig).WaitForOnline(); if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure."); var container = containers.Containers[0]; diff --git a/ProjectPlugins/CodexContractsPlugin/CoreInterfaceExtensions.cs b/ProjectPlugins/CodexContractsPlugin/CoreInterfaceExtensions.cs index ea123bc9..d07e25f7 100644 --- a/ProjectPlugins/CodexContractsPlugin/CoreInterfaceExtensions.cs +++ b/ProjectPlugins/CodexContractsPlugin/CoreInterfaceExtensions.cs @@ -1,13 +1,14 @@ -using Core; +using CodexClient; +using Core; using GethPlugin; namespace CodexContractsPlugin { public static class CoreInterfaceExtensions { - public static CodexContractsDeployment DeployCodexContracts(this CoreInterface ci, IGethNode gethNode) + public static CodexContractsDeployment DeployCodexContracts(this CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo) { - return Plugin(ci).DeployContracts(ci, gethNode); + return Plugin(ci).DeployContracts(ci, gethNode, versionInfo); } public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, IGethNode gethNode, CodexContractsDeployment deployment) @@ -15,17 +16,12 @@ namespace CodexContractsPlugin return Plugin(ci).WrapDeploy(gethNode, deployment); } - public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode) + public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo) { - var deployment = DeployCodexContracts(ci, gethNode); + var deployment = DeployCodexContracts(ci, gethNode, versionInfo); return WrapCodexContractsDeployment(ci, gethNode, deployment); } - public static void SetCodexDockerImageProvider(this CoreInterface ci, ICodexDockerImageProvider provider) - { - Plugin(ci).SetCodexDockerImageProvider(provider); - } - private static CodexContractsPlugin Plugin(CoreInterface ci) { return ci.GetPlugin(); diff --git a/ProjectPlugins/CodexContractsPlugin/VersionRegistry.cs b/ProjectPlugins/CodexContractsPlugin/VersionRegistry.cs deleted file mode 100644 index f52a38a8..00000000 --- a/ProjectPlugins/CodexContractsPlugin/VersionRegistry.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Diagnostics; -using Logging; - -namespace CodexContractsPlugin -{ - public interface ICodexDockerImageProvider - { - string GetCodexDockerImage(); - } - - public class VersionRegistry - { - private ICodexDockerImageProvider provider = new ExceptionProvider(); - private static readonly Dictionary cache = new Dictionary(); - private static readonly object cacheLock = new object(); - private readonly ILog log; - - public VersionRegistry(ILog log) - { - this.log = log; - } - - public void SetProvider(ICodexDockerImageProvider provider) - { - this.provider = provider; - } - - public string GetContractsDockerImage() - { - try - { - var codexImage = provider.GetCodexDockerImage(); - return GetContractsDockerImage(codexImage); - } - catch (Exception exc) - { - throw new Exception("Failed to get contracts docker image.", exc); - } - } - - private string GetContractsDockerImage(string codexImage) - { - lock (cacheLock) - { - if (cache.TryGetValue(codexImage, out string? value)) - { - return value; - } - var result = GetContractsImage(codexImage); - cache.Add(codexImage, result); - return result; - } - } - - private string GetContractsImage(string codexImage) - { - var inspectResult = InspectCodexImage(codexImage); - var image = ParseCodexContractsImageName(inspectResult); - log.Log($"From codex docker image '{codexImage}', determined codex-contracts docker image: '{image}'"); - return image; - } - - private string InspectCodexImage(string img) - { - Execute("docker", $"pull {img}"); - return Execute("docker", $"inspect {img}"); - } - - private string ParseCodexContractsImageName(string inspectResult) - { - // It is a nice json structure. But we only need this one line. - // "storage.codex.nim-codex.blockchain-image": "codexstorage/codex-contracts-eth:sha-0bf1385-dist-tests" - var lines = inspectResult.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var line = lines.Single(l => l.Contains("storage.codex.nim-codex.blockchain-image")); - var tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); - return tokens.Last().Replace("\"", "").Trim(); - } - - private string Execute(string cmd, string args) - { - var startInfo = new ProcessStartInfo( - fileName: cmd, - arguments: args - ); - startInfo.RedirectStandardOutput = true; - startInfo.RedirectStandardError = true; - - var process = Process.Start(startInfo); - if (process == null) - { - throw new Exception("Failed to start: " + cmd + args); - } - KillAfterTimeout(process); - - process.WaitForExit(); - return process.StandardOutput.ReadToEnd(); - } - - private void KillAfterTimeout(Process process) - { - // There's a known issue that some docker commands on some platforms - // will fail to stop on their own. This has been known since 2019 and it's not fixed. - // So we will issue a kill to the process ourselves if it exceeds a timeout. - - Task.Run(() => - { - Thread.Sleep(TimeSpan.FromSeconds(30.0)); - - if (process != null && !process.HasExited) - { - process.Kill(); - } - }); - } - } - - internal class ExceptionProvider : ICodexDockerImageProvider - { - public string GetCodexDockerImage() - { - throw new InvalidOperationException("CodexContractsPlugin has not yet received a CodexDockerImageProvider " + - "and so cannot select a compatible contracts docker image."); - } - } -} diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index 5ed36c92..bbb09c0b 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -1,8 +1,6 @@ -using CodexContractsPlugin; - -namespace CodexPlugin +namespace CodexPlugin { - public class CodexDockerImage : ICodexDockerImageProvider + public class CodexDockerImage { private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests"; diff --git a/ProjectPlugins/CodexPlugin/CodexPlugin.cs b/ProjectPlugins/CodexPlugin/CodexPlugin.cs index 277b6101..0c8f4510 100644 --- a/ProjectPlugins/CodexPlugin/CodexPlugin.cs +++ b/ProjectPlugins/CodexPlugin/CodexPlugin.cs @@ -42,7 +42,6 @@ namespace CodexPlugin public void Awake(IPluginAccess access) { - access.GetPlugin().SetCodexDockerImageProvider(codexDockerImage); } public void Announce() diff --git a/Tests/CodexReleaseTests/DataTests/DataExpiryTest.cs b/Tests/CodexReleaseTests/DataTests/DataExpiryTest.cs index 263a9e8b..4b9139e6 100644 --- a/Tests/CodexReleaseTests/DataTests/DataExpiryTest.cs +++ b/Tests/CodexReleaseTests/DataTests/DataExpiryTest.cs @@ -50,8 +50,9 @@ namespace CodexReleaseTests.DataTests var blockTtl = TimeSpan.FromMinutes(1.0); var interval = TimeSpan.FromSeconds(10.0); + var bootstrapNode = StartCodex(); var geth = StartGethNode(s => s.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); + var contracts = Ci.StartCodexContracts(geth, bootstrapNode.Version); var node = StartCodex(s => s .EnableMarketplace(geth, contracts, m => m.WithInitial(100.Eth(), 100.Tst())) .WithBlockTTL(blockTtl) diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index e6711e34..a5ec0885 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -6,7 +6,6 @@ using CodexTests; using DistTestCore; using GethPlugin; using Nethereum.Hex.HexConvertors.Extensions; -using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests @@ -21,14 +20,14 @@ namespace CodexReleaseTests.MarketTests { base.LifecycleStart(lifecycle); var geth = StartGethNode(s => s.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); + var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); handles.Add(lifecycle, new MarketplaceHandle(geth, contracts)); } protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) { - base.LifecycleStop(lifecycle, result); handles.Remove(lifecycle); + base.LifecycleStop(lifecycle, result); } protected IGethNode GetGeth() diff --git a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs index d39c31ec..d8ac6805 100644 --- a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs +++ b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs @@ -1,37 +1,40 @@ using CodexClient; using CodexPlugin; using DistTestCore; -using NUnit.Framework; namespace CodexTests { public class AutoBootstrapDistTest : CodexDistTest { private readonly Dictionary bootstrapNodes = new Dictionary(); + private bool isBooting = false; - [SetUp] - public void SetUpBootstrapNode() + protected override void LifecycleStart(TestLifecycle tl) { - var tl = Get(); + base.LifecycleStart(tl); if (!bootstrapNodes.ContainsKey(tl)) { + isBooting = true; bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace))); + isBooting = false; } } - [TearDown] - public void TearDownBootstrapNode() + protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) { - bootstrapNodes.Remove(Get()); + bootstrapNodes.Remove(lifecycle); + base.LifecycleStop(lifecycle, result); } protected override void OnCodexSetup(ICodexSetup setup) { + if (isBooting) return; + var node = BootstrapNode; if (node != null) setup.WithBootstrapNode(node); } - protected ICodexNode? BootstrapNode + protected ICodexNode BootstrapNode { get { @@ -40,7 +43,7 @@ namespace CodexTests { return node; } - return null; + throw new InvalidOperationException("Bootstrap node not yet started."); } } } diff --git a/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs b/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs index f8ef52d5..af213342 100644 --- a/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs +++ b/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs @@ -31,7 +31,7 @@ namespace ExperimentalTests.BasicTests ); var geth = StartGethNode(s => s.IsMiner().WithName("disttest-geth")); - var contracts = Ci.StartCodexContracts(geth); + var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); var numberOfHosts = 5; var hosts = StartCodex(numberOfHosts, s => s diff --git a/Tests/ExperimentalTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs b/Tests/ExperimentalTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs index ec128154..d0acc991 100644 --- a/Tests/ExperimentalTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs +++ b/Tests/ExperimentalTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs @@ -21,7 +21,7 @@ namespace ExperimentalTests.DownloadConnectivityTests public void MarketplaceDoesNotInterfereWithPeerDownload() { var geth = StartGethNode(s => s.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); + var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m .WithInitial(10.Eth(), 1000.TstWei()))); diff --git a/Tests/ExperimentalTests/PeerDiscoveryTests/PeerDiscoveryTests.cs b/Tests/ExperimentalTests/PeerDiscoveryTests/PeerDiscoveryTests.cs index 901504c7..60a9be9a 100644 --- a/Tests/ExperimentalTests/PeerDiscoveryTests/PeerDiscoveryTests.cs +++ b/Tests/ExperimentalTests/PeerDiscoveryTests/PeerDiscoveryTests.cs @@ -31,7 +31,7 @@ namespace ExperimentalTests.PeerDiscoveryTests public void MarketplaceDoesNotInterfereWithPeerDiscovery() { var geth = StartGethNode(s => s.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); + var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m .WithInitial(10.Eth(), 1000.TstWei()))); diff --git a/Tools/CodexNetDeployer/Deployer.cs b/Tools/CodexNetDeployer/Deployer.cs index 753a1371..0d78962e 100644 --- a/Tools/CodexNetDeployer/Deployer.cs +++ b/Tools/CodexNetDeployer/Deployer.cs @@ -64,8 +64,12 @@ namespace CodexNetDeployer var gethDeployment = DeployGeth(ci); var gethNode = ci.WrapGethDeployment(gethDeployment, new BlockCache()); + var bootNode = ci.StartCodexNode(); + var versionInfo = bootNode.GetDebugInfo().Version; + bootNode.Stop(waitTillStopped: true); + Log("Geth started. Deploying Codex contracts..."); - var contractsDeployment = ci.DeployCodexContracts(gethNode); + var contractsDeployment = ci.DeployCodexContracts(gethNode, versionInfo); var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment); Log("Codex contracts deployed."); From 9da87709c5f19e6172484fc87dc7694cb9df5030 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 22 Apr 2025 19:49:03 +0200 Subject: [PATCH 02/69] Implements using debug-info contracts version in codex-contracts plugin --- ProjectPlugins/CodexClient/Mapper.cs | 4 +--- ProjectPlugins/CodexClient/openapi.yaml | 3 +++ ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ProjectPlugins/CodexClient/Mapper.cs b/ProjectPlugins/CodexClient/Mapper.cs index 47b1215c..16c156bd 100644 --- a/ProjectPlugins/CodexClient/Mapper.cs +++ b/ProjectPlugins/CodexClient/Mapper.cs @@ -165,13 +165,11 @@ namespace CodexClient private DebugInfoVersion Map(CodexOpenApi.CodexVersion obj) { - throw new Exception("waiting for new codex image with contracts revision"); - return new DebugInfoVersion { Version = obj.Version, Revision = obj.Revision, - Contracts = "aaa" + Contracts = obj.Contracts }; } diff --git a/ProjectPlugins/CodexClient/openapi.yaml b/ProjectPlugins/CodexClient/openapi.yaml index fd66648e..d59cbcfa 100644 --- a/ProjectPlugins/CodexClient/openapi.yaml +++ b/ProjectPlugins/CodexClient/openapi.yaml @@ -124,6 +124,9 @@ components: revision: type: string example: 0c647d8 + contracts: + type: string + example: 0b537c7 PeersTable: type: object diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index 656c6588..4b190b34 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -10,7 +10,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "1A-F7-DF-C3-E1-C6-98-FF-32-20-21-9B-26-40-B0-51-08-35-C2-E7-DB-41-49-93-60-A9-CE-47-B5-AD-3D-A3"; + private const string OpenApiYamlHash = "06-B9-41-E8-C8-6C-DE-01-86-83-F3-9A-E4-AC-E7-30-D9-E6-64-60-E0-21-81-9E-4E-C5-93-77-2C-71-79-14"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; From c630d4c81ea2b0a5fb57abd05e7ebcc78074c391 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 23 Apr 2025 09:04:23 +0200 Subject: [PATCH 03/69] Asks user to set their eth address after passing a check --- .../CodexChecking/CodexTwoWayChecker.cs | 4 ++-- .../Commands/CheckResponseHandler.cs | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs index c5b9361a..83175803 100644 --- a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs +++ b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs @@ -8,7 +8,7 @@ namespace BiblioTech.CodexChecking public interface ICheckResponseHandler { Task CheckNotStarted(); - Task NowCompleted(ulong userId, string checkName); + Task NowCompleted(string checkName); Task GiveRoleReward(); Task InvalidData(); @@ -192,7 +192,7 @@ namespace BiblioTech.CodexChecking private async Task CheckNowCompleted(ICheckResponseHandler handler, TransferCheck check, ulong userId, string checkName) { - await handler.NowCompleted(userId, checkName); + await handler.NowCompleted(checkName); check.CompletedUtc = DateTime.UtcNow; repo.SaveChanges(); diff --git a/Tools/BiblioTech/Commands/CheckResponseHandler.cs b/Tools/BiblioTech/Commands/CheckResponseHandler.cs index 5428e86b..f7b1c4f0 100644 --- a/Tools/BiblioTech/Commands/CheckResponseHandler.cs +++ b/Tools/BiblioTech/Commands/CheckResponseHandler.cs @@ -74,10 +74,24 @@ namespace BiblioTech.Commands await context.Followup("The received data didn't match. Check has failed."); } - public async Task NowCompleted(ulong userId, string checkName) + public async Task NowCompleted(string checkName) { - await context.Followup("Successfully completed the check!"); - await Program.AdminChecker.SendInAdminChannel($"User <@{userId}> has completed check: {checkName}"); + // check if eth address is known for user. + var data = Program.UserRepo.GetUser(user); + if (data.CurrentAddress == null) + { + await context.Followup($"Successfully completed the check!{Environment.NewLine}" + + $"You haven't yet set your ethereum address. Consider using '/set' to set it.{Environment.NewLine}" + + $"(You can find your address in the 'eth.address' file of your Codex node.)"); + + await Program.AdminChecker.SendInAdminChannel($"User <@{user.Id}> has completed check: {checkName}" + + $" - EthAddress not set for user. User was reminded."); + } + else + { + await context.Followup("Successfully completed the check!"); + await Program.AdminChecker.SendInAdminChannel($"User <@{user.Id}> has completed check: {checkName}"); + } } public async Task ToAdminChannel(string msg) From 9446671b186d5dce78735ce684f5b20e94bb67b5 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 23 Apr 2025 09:07:19 +0200 Subject: [PATCH 04/69] Regenerates unique data for new checks --- .../BiblioTech/CodexChecking/CodexTwoWayChecker.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs index 83175803..ac83ccd9 100644 --- a/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs +++ b/Tools/BiblioTech/CodexChecking/CodexTwoWayChecker.cs @@ -37,7 +37,7 @@ namespace BiblioTech.CodexChecking public async Task StartDownloadCheck(ICheckResponseHandler handler, ulong userId) { var check = repo.GetOrCreate(userId).DownloadCheck; - if (string.IsNullOrEmpty(check.UniqueData)) + if (IsUniqueDataStale(check)) { check.UniqueData = GenerateUniqueData(); repo.SaveChanges(); @@ -69,7 +69,7 @@ namespace BiblioTech.CodexChecking public async Task StartUploadCheck(ICheckResponseHandler handler, ulong userId) { var check = repo.GetOrCreate(userId).UploadCheck; - if (string.IsNullOrEmpty(check.UniqueData)) + if (IsUniqueDataStale(check)) { check.UniqueData = GenerateUniqueData(); repo.SaveChanges(); @@ -111,6 +111,15 @@ namespace BiblioTech.CodexChecking return $"{RandomBusyMessage.Get().Substring(5)}{RandomUtils.GenerateRandomString(12)}"; } + private bool IsUniqueDataStale(TransferCheck check) + { + var expiry = DateTime.UtcNow - TimeSpan.FromMinutes(10.0); + + return + string.IsNullOrEmpty(check.UniqueData) || + check.CompletedUtc < expiry; + } + private string UploadData(string uniqueData) { var filePath = Path.Combine(config.ChecksDataPath, Guid.NewGuid().ToString()); From 7eb3bbd1deeb4cb6bd7d95561b4ef1294d0696a2 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 23 Apr 2025 10:22:23 +0200 Subject: [PATCH 05/69] Setup example in lifecycelytest --- Tests/CodexReleaseTests/Parallelism.cs | 2 +- Tests/FrameworkTests/LifecycelyTest.cs | 226 +++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 Tests/FrameworkTests/LifecycelyTest.cs diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index a1b26c73..f6e68cc0 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; [assembly: LevelOfParallelism(1)] -namespace CodexReleaseTests.DataTests +namespace CodexReleaseTests { } diff --git a/Tests/FrameworkTests/LifecycelyTest.cs b/Tests/FrameworkTests/LifecycelyTest.cs new file mode 100644 index 00000000..ef90a48c --- /dev/null +++ b/Tests/FrameworkTests/LifecycelyTest.cs @@ -0,0 +1,226 @@ +using NUnit.Framework; + +namespace FrameworkTests +{ + [Parallelizable(ParallelScope.All)] + [TestFixture(10)] + [TestFixture(20)] + [TestFixture(30)] + public class LifecycelyTest + { + public LifecycelyTest(int num) + { + Log("ctor", GetCurrentTestName(), num); + this.num = num; + } + + [SetUp] + public void Setup() + { + Log(nameof(Setup), GetCurrentTestName()); + } + + [TearDown] + public void TearDown() + { + Log(nameof(TearDown), GetCurrentTestName()); + } + + [Test] + public void A() + { + Log(nameof(A), "Run"); + SleepRandom(); + Log(nameof(A), "Finish"); + } + + [Test] + public void B() + { + Log(nameof(B), "Run"); + SleepRandom(); + Log(nameof(B), "Finish"); + } + + [Test] + public void C() + { + Log(nameof(C), "Run"); + SleepRandom(); + Log(nameof(C), "Finish"); + } + + [Test] + [Combinatorial] + public void Multi( + [Values(1, 2, 3)] int num) + { + Log(nameof(Multi), "Run", num); + SleepRandom(); + Log(nameof(Multi), "Finish", num); + } + + + + + + + + + + + + + private static readonly Random r = new Random(); + private readonly int num; + + private void SleepRandom() + { + Thread.Sleep(TimeSpan.FromSeconds(5.0)); + Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); + } + + private void Log(string scope, string msg) + { + ALog.Log($"{num} {scope} {msg}"); + } + + private void Log(string scope, string msg, int num) + { + ALog.Log($"{this.num} {scope} {msg} {num}"); + } + + private string GetCurrentTestName() + { + return $"[{TestContext.CurrentContext.Test.Name}]"; + } + } + + + + + public class ALog + { + private static readonly object _lock = new object(); + + public static void Log(string msg) + { + lock (_lock) + { + File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); + } + } + } + + + + + + + + + + + + public interface ITestLifecycleComponent + { + void Start(); + void Stop(string results); + } + + + public class Base + { + private readonly Dictionary> anyFields = new(); + + public void Setup() + { + var testId = 23; + + var fields = new Dictionary(); + anyFields.Add(testId, fields); + YieldFields(field => + { + fields.Add(field.GetType(), field); + }); + + foreach (var field in fields.Values) + { + field.Start(); + } + } + + public void TearDown() + { + var testId = 23; + + // foreach stop + + anyFields.Remove(testId); + } + + public T Get() + { + int testId = 123; + var fields = anyFields[testId]; + var type = typeof(T); + var result = fields[type]; + return (T)result; + } + + public BaseFields GetBaseField() + { + return Get(); + } + + protected virtual void YieldFields(Action giveField) + { + giveField(new BaseFields()); + } + } + + public class Mid : Base + { + protected override void YieldFields(Action giveField) + { + base.YieldFields(giveField); + giveField(new MidFields()); + } + + public MidFields GetMid() + { + return Get(); + } + } + + public class Top : Mid + { + protected override void YieldFields(Action giveField) + { + base.YieldFields(giveField); + giveField(new TopFields()); + } + + public TopFields GetTop() + { + return Get(); + } + } + + public class BaseFields : ITestLifecycleComponent + { + public string EntryPoint { get; set; } = string.Empty; + public string Log { get; set; } = string.Empty; + } + + public class MidFields : ITestLifecycleComponent + { + public string Nodes { get; set; } = string.Empty; + } + + public class TopFields : ITestLifecycleComponent + { + public string Geth { get; set; } = string.Empty; + public string Contracts { get; set; } = string.Empty; + } +} From 2dfdfac2bb52e4ff919ee5f1b3986a839d71b0a2 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 23 Apr 2025 14:18:11 +0200 Subject: [PATCH 06/69] while better the new plan is still a mess --- .../OverwatchSupport/CodexTranscriptWriter.cs | 15 +- .../CodexTranscriptWriterConfig.cs | 4 +- .../MarketplaceAutoBootstrapDistTest.cs | 39 +- Tests/DistTestCore/DistTest.cs | 124 ++----- .../DistTestLifecycleComponents.cs | 97 +++++ Tests/DistTestCore/Logs/FixtureLog.cs | 13 +- Tests/DistTestCore/Logs/TestLog.cs | 2 - Tests/DistTestCore/TestLifecycle.cs | 30 +- .../AutoBootstrapDistTest.cs | 58 +-- Tests/ExperimentalTests/CodexDistTest.cs | 215 ++++------- .../CodexLogTrackerProvider.cs | 73 ++++ Tests/FrameworkTests/LifecycelyTest.cs | 334 +++++++++--------- Tests/FrameworkTests/Parallelism.cs | 6 + 13 files changed, 549 insertions(+), 461 deletions(-) create mode 100644 Tests/DistTestCore/DistTestLifecycleComponents.cs create mode 100644 Tests/ExperimentalTests/CodexLogTrackerProvider.cs create mode 100644 Tests/FrameworkTests/Parallelism.cs diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs index afd8d1a2..53ac1496 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs @@ -23,16 +23,27 @@ namespace CodexPlugin.OverwatchSupport converter = new CodexLogConverter(writer, config, identityMap); } - public void Finalize(string outputFilepath) + public void Finalize() { log.Log("Finalizing Codex transcript..."); writer.AddHeader(CodexHeaderKey, CreateCodexHeader()); - writer.Write(outputFilepath); + writer.Write(GetOutputFullPath()); log.Log("Done"); } + private string GetOutputFullPath() + { + var outputPath = Path.GetDirectoryName(log.GetFullName()); + if (outputPath == null) throw new Exception("Logfile path is null"); + var filename = Path.GetFileNameWithoutExtension(log.GetFullName()); + if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); + var outputFile = Path.Combine(outputPath, filename + "_" + config.OutputFilename); + if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; + return outputFile; + } + public ICodexNodeHooks CreateHooks(string nodeName) { nodeName = Str.Between(nodeName, "'", "'"); diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs index 247494c8..8c5e7bf0 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs @@ -2,11 +2,13 @@ { public class CodexTranscriptWriterConfig { - public CodexTranscriptWriterConfig(bool includeBlockReceivedEvents) + public CodexTranscriptWriterConfig(string outputFilename, bool includeBlockReceivedEvents) { + OutputFilename = outputFilename; IncludeBlockReceivedEvents = includeBlockReceivedEvents; } + public string OutputFilename { get; } public bool IncludeBlockReceivedEvents { get; } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index a5ec0885..f6e8a9ca 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -10,18 +10,41 @@ using Utils; namespace CodexReleaseTests.MarketTests { + public class MarketplaceTestComponent : ILifecycleComponent + { + public IGethNode Geth { get; } + public ICodexContracts Contracts { get; } + + public void Start(ILifecycleComponentAccess access) + { + throw new NotImplementedException(); + } + + public void Stop(ILifecycleComponentAccess access, DistTestResult result) + { + throw new NotImplementedException(); + } + } + public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest { - private readonly Dictionary handles = new Dictionary(); protected const int StartingBalanceTST = 1000; protected const int StartingBalanceEth = 10; + protected override void CreateComponents(ILifecycleComponentCollector collector) + { + base.CreateComponents(collector); + + + collector.AddComponent(new MarketplaceTestComponent()); + } + protected override void LifecycleStart(TestLifecycle lifecycle) { base.LifecycleStart(lifecycle); var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); - handles.Add(lifecycle, new MarketplaceHandle(geth, contracts)); + handles.Add(lifecycle, new MarketplaceTestComponent(geth, contracts)); } protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) @@ -323,17 +346,5 @@ namespace CodexReleaseTests.MarketTests public SlotFilledEventDTO SlotFilledEvent { get; } public ICodexNode Host { get; } } - - private class MarketplaceHandle - { - public MarketplaceHandle(IGethNode geth, ICodexContracts contracts) - { - Geth = geth; - Contracts = contracts; - } - - public IGethNode Geth { get; } - public ICodexContracts Contracts { get; } - } } } diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 534eb74a..9a969532 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -19,9 +19,8 @@ namespace DistTestCore private readonly Assembly[] testAssemblies; private readonly FixtureLog fixtureLog; private readonly StatusLog statusLog; - private readonly object lifecycleLock = new object(); private readonly EntryPoint globalEntryPoint; - private readonly Dictionary lifecycles = new Dictionary(); + private readonly DistTestLifecycleComponents lifecycleComponents = new DistTestLifecycleComponents(); private readonly string deployId; public DistTest() @@ -89,7 +88,12 @@ namespace DistTestCore { try { - CreateNewTestLifecycle(); + var testName = GetCurrentTestName(); + fixtureLog.WriteLogTag(); + Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () => + { + lifecycleComponents.Setup(testName, CreateComponents); + }); } catch (Exception ex) { @@ -104,7 +108,7 @@ namespace DistTestCore { try { - DisposeTestLifecycle(); + Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", DisposeTestLifecycle); } catch (Exception ex) { @@ -171,80 +175,49 @@ namespace DistTestCore { } - protected virtual void LifecycleStart(TestLifecycle lifecycle) + protected virtual void CreateComponents(ILifecycleComponentCollector collector) + { + var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); + var lifecycle = new TestLifecycle( + fixtureLog.CreateTestLog(), + configuration, + GetWebCallTimeSet(), + GetK8sTimeSet(), + testNamespace, + GetCurrentTestName(), + deployId, + ShouldWaitForCleanup()); + + collector.AddComponent(lifecycle); + } + + protected virtual void DestroyComponents(TestLifecycle lifecycle, DistTestResult testResult) { } - protected virtual void LifecycleStop(TestLifecycle lifecycle, DistTestResult testResult) + public T Get() where T : ILifecycleComponent { + return lifecycleComponents.Get(GetCurrentTestName()); } - protected virtual void CollectStatusLogData(TestLifecycle lifecycle, Dictionary data) + private TestLifecycle Get() { - } - - protected TestLifecycle Get() - { - lock (lifecycleLock) - { - return lifecycles[GetCurrentTestName()]; - } - } - - private void CreateNewTestLifecycle() - { - var testName = GetCurrentTestName(); - fixtureLog.WriteLogTag(); - Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () => - { - lock (lifecycleLock) - { - var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); - var lifecycle = new TestLifecycle( - fixtureLog.CreateTestLog(), - configuration, - GetWebCallTimeSet(), - GetK8sTimeSet(), - testNamespace, - deployId, - ShouldWaitForCleanup()); - lifecycles.Add(testName, lifecycle); - LifecycleStart(lifecycle); - } - }); + return Get(); } private void DisposeTestLifecycle() { + var testName = GetCurrentTestName(); + var results = GetTestResult(); var lifecycle = Get(); - var testResult = GetTestResult(); var testDuration = lifecycle.GetTestDuration(); var data = lifecycle.GetPluginMetadata(); - CollectStatusLogData(lifecycle, data); - fixtureLog.Log($"{GetCurrentTestName()} = {testResult} ({testDuration})"); - statusLog.ConcludeTest(testResult, testDuration, data); - Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", () => - { - WriteEndTestLog(lifecycle.Log); + fixtureLog.Log($"{GetCurrentTestName()} = {results} ({testDuration})"); + statusLog.ConcludeTest(results, testDuration, data); - IncludeLogsOnTestFailure(lifecycle); - LifecycleStop(lifecycle, testResult); - lifecycle.DeleteAllResources(); - lifecycles.Remove(GetCurrentTestName()); - }); + lifecycleComponents.TearDown(testName, results); } - private void WriteEndTestLog(TestLog log) - { - var result = TestContext.CurrentContext.Result; - - Log($"*** Finished: {GetCurrentTestName()} = {result.Outcome.Status}"); - if (!string.IsNullOrEmpty(result.Message)) - { - Log(result.Message); - Log($"{result.StackTrace}"); - } - } private IWebCallTimeSet GetWebCallTimeSet() { @@ -296,28 +269,6 @@ namespace DistTestCore .ToArray(); } - private void IncludeLogsOnTestFailure(TestLifecycle lifecycle) - { - var testStatus = TestContext.CurrentContext.Result.Outcome.Status; - if (ShouldDownloadAllLogs(testStatus)) - { - lifecycle.Log.Log("Downloading all container logs..."); - lifecycle.DownloadAllLogs(); - } - } - - private bool ShouldDownloadAllLogs(TestStatus testStatus) - { - if (configuration.AlwaysDownloadContainerLogs) return true; - if (!IsDownloadingLogsEnabled()) return false; - if (testStatus == TestStatus.Failed) - { - return true; - } - - return false; - } - private string GetCurrentTestName() { return $"[{TestContext.CurrentContext.Test.Name}]"; @@ -328,7 +279,8 @@ namespace DistTestCore var success = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed; var status = TestContext.CurrentContext.Result.Outcome.Status.ToString(); var result = TestContext.CurrentContext.Result.Message; - return new DistTestResult(success, status, result ?? string.Empty); + var trace = TestContext.CurrentContext.Result.StackTrace; + return new DistTestResult(success, status, result ?? string.Empty, trace ?? string.Empty); } private bool IsDownloadingLogsEnabled() @@ -339,16 +291,18 @@ namespace DistTestCore public class DistTestResult { - public DistTestResult(bool success, string status, string result) + public DistTestResult(bool success, string status, string result, string trace) { Success = success; Status = status; Result = result; + Trace = trace; } public bool Success { get; } public string Status { get; } public string Result { get; } + public string Trace { get; } public override string ToString() { diff --git a/Tests/DistTestCore/DistTestLifecycleComponents.cs b/Tests/DistTestCore/DistTestLifecycleComponents.cs new file mode 100644 index 00000000..203a891c --- /dev/null +++ b/Tests/DistTestCore/DistTestLifecycleComponents.cs @@ -0,0 +1,97 @@ +namespace DistTestCore +{ + public interface ILifecycleComponent + { + void Start(ILifecycleComponentAccess access); + void Stop(ILifecycleComponentAccess access, DistTestResult result); + } + + public interface ILifecycleComponentCollector + { + void AddComponent(ILifecycleComponent component); + } + + public interface ILifecycleComponentAccess + { + T Get() where T : ILifecycleComponent; + } + + public class DistTestLifecycleComponents + { + private readonly object _lock = new object(); + private readonly Dictionary> components = new(); + + public void Setup(string testName, Action initializer) + { + var newComponents = new Dictionary(); + lock (_lock) + { + components.Add(testName, newComponents); + var collector = new Collector(newComponents); + initializer(collector); + } + + var access = new ScopedAccess(this, testName); + foreach (var component in newComponents.Values) + { + component.Start(access); + } + } + + public T Get(string testName) where T : ILifecycleComponent + { + var type = typeof(T); + lock (_lock) + { + return (T)components[testName][type]; + } + } + + public void TearDown(string testName, DistTestResult result) + { + var access = new ScopedAccess(this, testName); + var closingComponents = components[testName]; + foreach (var component in closingComponents.Values) + { + component.Stop(access, result); + } + + lock (_lock) + { + components.Remove(testName); + } + } + + private class Collector : ILifecycleComponentCollector + { + private readonly Dictionary components; + + public Collector(Dictionary components) + { + this.components = components; + } + + public void AddComponent(ILifecycleComponent component) + { + components.Add(component.GetType(), component); + } + } + + private class ScopedAccess : ILifecycleComponentAccess + { + private readonly DistTestLifecycleComponents parent; + private readonly string testName; + + public ScopedAccess(DistTestLifecycleComponents parent, string testName) + { + this.parent = parent; + this.testName = testName; + } + + public T Get() where T : ILifecycleComponent + { + return parent.Get(testName); + } + } + } +} diff --git a/Tests/DistTestCore/Logs/FixtureLog.cs b/Tests/DistTestCore/Logs/FixtureLog.cs index 9d3c77d3..f90147d8 100644 --- a/Tests/DistTestCore/Logs/FixtureLog.cs +++ b/Tests/DistTestCore/Logs/FixtureLog.cs @@ -4,26 +4,25 @@ namespace DistTestCore.Logs { public class FixtureLog : BaseTestLog { - private readonly ILog backingLog; - private readonly string deployId; - public FixtureLog(ILog backingLog, string deployId) : base(backingLog, deployId) { - this.backingLog = backingLog; - this.deployId = deployId; } public TestLog CreateTestLog(string name = "") { - return TestLog.Create(this, name); + var result = TestLog.Create(this, name); + result.Log(NameUtils.GetRawFixtureName()); + return result; } public static FixtureLog Create(LogConfig config, DateTime start, string deployId, string name = "") { var fullName = NameUtils.GetFixtureFullName(config, start, name); var log = CreateMainLog(fullName, name); - return new FixtureLog(log, deployId); + var result = new FixtureLog(log, deployId); + result.Log(NameUtils.GetRawFixtureName()); + return result; } } } diff --git a/Tests/DistTestCore/Logs/TestLog.cs b/Tests/DistTestCore/Logs/TestLog.cs index 0dca1464..6a334138 100644 --- a/Tests/DistTestCore/Logs/TestLog.cs +++ b/Tests/DistTestCore/Logs/TestLog.cs @@ -1,5 +1,4 @@ using Logging; -using System.Xml.Linq; namespace DistTestCore.Logs { @@ -8,7 +7,6 @@ namespace DistTestCore.Logs public TestLog(ILog backingLog, string methodName, string deployId, string name = "") : base(backingLog, deployId) { - backingLog.Log($"*** Begin: {methodName}"); } public static TestLog Create(FixtureLog parentLog, string name = "") diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 3d642d20..550b6151 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -10,22 +10,24 @@ using WebUtils; namespace DistTestCore { - public class TestLifecycle : IK8sHooks + public class TestLifecycle : IK8sHooks, ILifecycleComponent { private const string TestsType = "dist-tests"; private readonly EntryPoint entryPoint; private readonly Dictionary metadata; private readonly List runningContainers = new(); + private readonly string testName; private readonly string deployId; private readonly List stoppedContainerLogs = new List(); - public TestLifecycle(TestLog log, Configuration configuration, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8sTimeSet, string testNamespace, string deployId, bool waitForCleanup) + public TestLifecycle(TestLog log, Configuration configuration, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8sTimeSet, string testNamespace, string testName, string deployId, bool waitForCleanup) { Log = log; Configuration = configuration; WebCallTimeSet = webCallTimeSet; K8STimeSet = k8sTimeSet; TestNamespace = testNamespace; + this.testName = testName; TestStart = DateTime.UtcNow; entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(k8sTimeSet, this, testNamespace), configuration.GetFileManagerFolder(), webCallTimeSet, k8sTimeSet); @@ -36,6 +38,28 @@ namespace DistTestCore log.WriteLogTag(); } + public void Start(ILifecycleComponentAccess access) + { + Log.Log($"*** Begin: {testName}"); + } + + public void Stop(ILifecycleComponentAccess access, DistTestResult result) + { + Log.Log($"*** Finished: {testName} = {result.Status}"); + if (!string.IsNullOrEmpty(result.Result)) + { + Log.Log(result.Result); + Log.Log($"{result.Trace}"); + } + + if (!result.Success) + { + DownloadAllLogs(); + } + + DeleteAllResources(); + } + public DateTime TestStart { get; } public TestLog Log { get; } public Configuration Configuration { get; } @@ -45,7 +69,7 @@ namespace DistTestCore public bool WaitForCleanup { get; } public CoreInterface CoreInterface { get; } - public void DeleteAllResources() + private void DeleteAllResources() { entryPoint.Decommission( deleteKubernetesResources: true, diff --git a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs index d8ac6805..a2477597 100644 --- a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs +++ b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs @@ -4,46 +4,56 @@ using DistTestCore; namespace CodexTests { - public class AutoBootstrapDistTest : CodexDistTest + public class AutoBootstrapComponent : ILifecycleComponent { - private readonly Dictionary bootstrapNodes = new Dictionary(); - private bool isBooting = false; + public ICodexNode? BootstrapNode { get; private set; } = null; - protected override void LifecycleStart(TestLifecycle tl) + public void Start(ILifecycleComponentAccess access) { - base.LifecycleStart(tl); - if (!bootstrapNodes.ContainsKey(tl)) - { - isBooting = true; - bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace))); - isBooting = false; - } + if (BootstrapNode != null) return; + + var tl = access.Get(); + var ci = tl.CoreInterface; + var testNamespace = tl.TestNamespace; + + BootstrapNode = ci.StartCodexNode(s => s.WithName("BOOTSTRAP_" + testNamespace)); } - protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) + public void ApplyBootstrapNode(ICodexSetup setup) { - bootstrapNodes.Remove(lifecycle); - base.LifecycleStop(lifecycle, result); + if (BootstrapNode == null) return; + + setup.WithBootstrapNode(BootstrapNode); + } + + public void Stop(ILifecycleComponentAccess access, DistTestResult result) + { + if (BootstrapNode == null) return; + BootstrapNode.Stop(waitTillStopped: false); + } + } + + public class AutoBootstrapDistTest : CodexDistTest + { + + protected override void CreateComponents(ILifecycleComponentCollector collector) + { + base.CreateComponents(collector); + collector.AddComponent(new AutoBootstrapComponent()); } protected override void OnCodexSetup(ICodexSetup setup) { - if (isBooting) return; - - var node = BootstrapNode; - if (node != null) setup.WithBootstrapNode(node); + Get().ApplyBootstrapNode(setup); } protected ICodexNode BootstrapNode { get { - var tl = Get(); - if (bootstrapNodes.TryGetValue(tl, out var node)) - { - return node; - } - throw new InvalidOperationException("Bootstrap node not yet started."); + var bn = Get().BootstrapNode; + if (bn == null) throw new InvalidOperationException("BootstrapNode accessed before initialized."); + return bn; } } } diff --git a/Tests/ExperimentalTests/CodexDistTest.cs b/Tests/ExperimentalTests/CodexDistTest.cs index d27216d3..40e711fc 100644 --- a/Tests/ExperimentalTests/CodexDistTest.cs +++ b/Tests/ExperimentalTests/CodexDistTest.cs @@ -1,6 +1,5 @@ using BlockchainUtils; using CodexClient; -using CodexClient.Hooks; using CodexContractsPlugin; using CodexNetDeployer; using CodexPlugin; @@ -17,86 +16,74 @@ using Newtonsoft.Json; using NUnit.Framework; using NUnit.Framework.Constraints; using OverwatchTranscript; -using Utils; namespace CodexTests { - public class CodexLogTrackerProvider : ICodexHooksProvider + public class CodexDistTestComponents : ILifecycleComponent { - private readonly Action addNode; + private readonly object nodesLock = new object(); - public CodexLogTrackerProvider(Action addNode) + public CodexDistTestComponents(CodexTranscriptWriter? writer) { - this.addNode = addNode; + Writer = writer; } - // See TestLifecycle.cs DownloadAllLogs() - public ICodexNodeHooks CreateHooks(string nodeName) + public CodexTranscriptWriter? Writer { get; } + public BlockCache Cache { get; } = new(); + public List Nodes { get; } = new(); + + public void Start(ILifecycleComponentAccess access) { - return new CodexLogTracker(addNode); + var ci = access.Get().CoreInterface; + ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n => + { + lock (nodesLock) + { + Nodes.Add(n); + } + })); } - public class CodexLogTracker : ICodexNodeHooks + public void Stop(ILifecycleComponentAccess access, DistTestResult result) { - private readonly Action addNode; + var tl = access.Get(); + var log = tl.Log; + var logFiles = tl.DownloadAllLogs(); - public CodexLogTracker(Action addNode) + TeardownTranscript(log, logFiles, result); + + // todo: on not success: go to nodes and dl logs? + // or fix disttest failure log download so we can always have logs even for non-codexes? + } + + private void TeardownTranscript(TestLog log, IDownloadedLog[] logFiles, DistTestResult result) + { + if (Writer == null) return; + + Writer.AddResult(result.Success, result.Result); + + try { - this.addNode = addNode; + Stopwatch.Measure(log, "Transcript.ProcessLogs", () => + { + Writer.ProcessLogs(logFiles); + }); + + Stopwatch.Measure(log, $"Transcript.Finalize", () => + { + Writer.IncludeFile(log.GetFullName()); + Writer.Finalize(); + }); } - - public void OnFileDownloaded(ByteSize size, ContentId cid) - { - } - - public void OnFileDownloading(ContentId cid) - { - } - - public void OnFileUploaded(string uid, ByteSize size, ContentId cid) - { - } - - public void OnFileUploading(string uid, ByteSize size) - { - } - - public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) - { - addNode(node); - } - - public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) - { - } - - public void OnNodeStopping() - { - } - - public void OnStorageAvailabilityCreated(StorageAvailability response) - { - } - - public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) - { - } - - public void OnStorageContractUpdated(StoragePurchase purchaseStatus) + catch (Exception ex) { + log.Error("Failure during transcript teardown: " + ex); } } } public class CodexDistTest : DistTest { - private static readonly object _lock = new object(); - private static readonly Dictionary writers = new Dictionary(); - private static readonly Dictionary blockCaches = new Dictionary(); - - // this entire structure is not good and needs to be destroyed at the earliest convenience: - private static readonly Dictionary> nodes = new Dictionary>(); - public CodexDistTest() { ProjectPlugin.Load(); @@ -112,34 +99,12 @@ namespace CodexTests localBuilder.Build(); } - protected override void LifecycleStart(TestLifecycle lifecycle) + protected override void CreateComponents(ILifecycleComponentCollector collector) { - base.LifecycleStart(lifecycle); - SetupTranscript(lifecycle); - - Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n => - { - lock (_lock) - { - if (!nodes.ContainsKey(lifecycle)) nodes.Add(lifecycle, new List()); - nodes[lifecycle].Add(n); - } - })); - } - - protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) - { - base.LifecycleStop(lifecycle, result); - TeardownTranscript(lifecycle, result); - - if (!result.Success) - { - lock (_lock) - { - var codexNodes = nodes[lifecycle]; - foreach (var node in codexNodes) node.DownloadLog(); - } - } + base.CreateComponents(collector); + collector.AddComponent(new CodexDistTestComponents( + SetupTranscript() + )); } public ICodexNode StartCodex() @@ -173,6 +138,11 @@ namespace CodexTests return Ci.StartGethNode(GetBlockCache(), setup); } + private BlockCache GetBlockCache() + { + return Get().Cache; + } + public PeerConnectionTestHelpers CreatePeerConnectionTestHelpers() { return new PeerConnectionTestHelpers(GetTestLog()); @@ -180,7 +150,7 @@ namespace CodexTests public PeerDownloadTestHelpers CreatePeerDownloadTestHelpers() { - return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); + return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); } public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "") @@ -258,81 +228,20 @@ namespace CodexTests return null; } - private void SetupTranscript(TestLifecycle lifecycle) + private CodexTranscriptWriter? SetupTranscript() { var attr = GetTranscriptAttributeOfCurrentTest(); - if (attr == null) return; + if (attr == null) return null; var config = new CodexTranscriptWriterConfig( + attr.OutputFilename, attr.IncludeBlockReceivedEvents ); - var log = new LogPrefixer(lifecycle.Log, "(Transcript) "); + var log = new LogPrefixer(GetTestLog(), "(Transcript) "); var writer = new CodexTranscriptWriter(log, config, Transcript.NewWriter(log)); Ci.AddCodexHooksProvider(writer); - lock (_lock) - { - writers.Add(lifecycle, writer); - } - } - - private void TeardownTranscript(TestLifecycle lifecycle, DistTestResult result) - { - var attr = GetTranscriptAttributeOfCurrentTest(); - if (attr == null) return; - - var outputFilepath = GetOutputFullPath(lifecycle, attr); - - CodexTranscriptWriter writer = null!; - lock (_lock) - { - writer = writers[lifecycle]; - writers.Remove(lifecycle); - } - - writer.AddResult(result.Success, result.Result); - - try - { - Stopwatch.Measure(lifecycle.Log, "Transcript.ProcessLogs", () => - { - writer.ProcessLogs(lifecycle.DownloadAllLogs()); - }); - - Stopwatch.Measure(lifecycle.Log, $"Transcript.Finalize: {outputFilepath}", () => - { - writer.IncludeFile(lifecycle.Log.GetFullName()); - writer.Finalize(outputFilepath); - }); - } - catch (Exception ex) - { - lifecycle.Log.Error("Failure during transcript teardown: " + ex); - } - } - - private string GetOutputFullPath(TestLifecycle lifecycle, CreateTranscriptAttribute attr) - { - var outputPath = Path.GetDirectoryName(lifecycle.Log.GetFullName()); - if (outputPath == null) throw new Exception("Logfile path is null"); - var filename = Path.GetFileNameWithoutExtension(lifecycle.Log.GetFullName()); - if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); - var outputFile = Path.Combine(outputPath, filename + "_" + attr.OutputFilename); - if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; - return outputFile; - } - - private BlockCache GetBlockCache() - { - var lifecycle = Get(); - lock (_lock) - { - if (!blockCaches.ContainsKey(lifecycle)) - { - blockCaches[lifecycle] = new BlockCache(); - } - } - return blockCaches[lifecycle]; + return writer; } } diff --git a/Tests/ExperimentalTests/CodexLogTrackerProvider.cs b/Tests/ExperimentalTests/CodexLogTrackerProvider.cs new file mode 100644 index 00000000..1b2fdd66 --- /dev/null +++ b/Tests/ExperimentalTests/CodexLogTrackerProvider.cs @@ -0,0 +1,73 @@ +using CodexClient; +using CodexClient.Hooks; +using Utils; + +namespace CodexTests +{ + public class CodexLogTrackerProvider : ICodexHooksProvider + { + private readonly Action addNode; + + public CodexLogTrackerProvider(Action addNode) + { + this.addNode = addNode; + } + + // See TestLifecycle.cs DownloadAllLogs() + public ICodexNodeHooks CreateHooks(string nodeName) + { + return new CodexLogTracker(addNode); + } + + public class CodexLogTracker : ICodexNodeHooks + { + private readonly Action addNode; + + public CodexLogTracker(Action addNode) + { + this.addNode = addNode; + } + + public void OnFileDownloaded(ByteSize size, ContentId cid) + { + } + + public void OnFileDownloading(ContentId cid) + { + } + + public void OnFileUploaded(string uid, ByteSize size, ContentId cid) + { + } + + public void OnFileUploading(string uid, ByteSize size) + { + } + + public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) + { + addNode(node); + } + + public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) + { + } + + public void OnNodeStopping() + { + } + + public void OnStorageAvailabilityCreated(StorageAvailability response) + { + } + + public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) + { + } + + public void OnStorageContractUpdated(StoragePurchase purchaseStatus) + { + } + } + } +} diff --git a/Tests/FrameworkTests/LifecycelyTest.cs b/Tests/FrameworkTests/LifecycelyTest.cs index ef90a48c..6c11e14f 100644 --- a/Tests/FrameworkTests/LifecycelyTest.cs +++ b/Tests/FrameworkTests/LifecycelyTest.cs @@ -1,64 +1,64 @@ -using NUnit.Framework; +//using NUnit.Framework; -namespace FrameworkTests -{ - [Parallelizable(ParallelScope.All)] - [TestFixture(10)] - [TestFixture(20)] - [TestFixture(30)] - public class LifecycelyTest - { - public LifecycelyTest(int num) - { - Log("ctor", GetCurrentTestName(), num); - this.num = num; - } +//namespace FrameworkTests +//{ +// [Parallelizable(ParallelScope.All)] +// [TestFixture(10)] +// [TestFixture(20)] +// [TestFixture(30)] +// public class LifecycelyTest +// { +// public LifecycelyTest(int num) +// { +// Log("ctor", GetCurrentTestName(), num); +// this.num = num; +// } - [SetUp] - public void Setup() - { - Log(nameof(Setup), GetCurrentTestName()); - } +// [SetUp] +// public void Setup() +// { +// Log(nameof(Setup), GetCurrentTestName()); +// } - [TearDown] - public void TearDown() - { - Log(nameof(TearDown), GetCurrentTestName()); - } +// [TearDown] +// public void TearDown() +// { +// Log(nameof(TearDown), GetCurrentTestName()); +// } - [Test] - public void A() - { - Log(nameof(A), "Run"); - SleepRandom(); - Log(nameof(A), "Finish"); - } +// [Test] +// public void A() +// { +// Log(nameof(A), "Run"); +// SleepRandom(); +// Log(nameof(A), "Finish"); +// } - [Test] - public void B() - { - Log(nameof(B), "Run"); - SleepRandom(); - Log(nameof(B), "Finish"); - } +// [Test] +// public void B() +// { +// Log(nameof(B), "Run"); +// SleepRandom(); +// Log(nameof(B), "Finish"); +// } - [Test] - public void C() - { - Log(nameof(C), "Run"); - SleepRandom(); - Log(nameof(C), "Finish"); - } +// [Test] +// public void C() +// { +// Log(nameof(C), "Run"); +// SleepRandom(); +// Log(nameof(C), "Finish"); +// } - [Test] - [Combinatorial] - public void Multi( - [Values(1, 2, 3)] int num) - { - Log(nameof(Multi), "Run", num); - SleepRandom(); - Log(nameof(Multi), "Finish", num); - } +// [Test] +// [Combinatorial] +// public void Multi( +// [Values(1, 2, 3)] int num) +// { +// Log(nameof(Multi), "Run", num); +// SleepRandom(); +// Log(nameof(Multi), "Finish", num); +// } @@ -71,46 +71,46 @@ namespace FrameworkTests - private static readonly Random r = new Random(); - private readonly int num; +// private static readonly Random r = new Random(); +// private readonly int num; - private void SleepRandom() - { - Thread.Sleep(TimeSpan.FromSeconds(5.0)); - Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); - } +// private void SleepRandom() +// { +// Thread.Sleep(TimeSpan.FromSeconds(5.0)); +// Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); +// } - private void Log(string scope, string msg) - { - ALog.Log($"{num} {scope} {msg}"); - } +// private void Log(string scope, string msg) +// { +// ALog.Log($"{num} {scope} {msg}"); +// } - private void Log(string scope, string msg, int num) - { - ALog.Log($"{this.num} {scope} {msg} {num}"); - } +// private void Log(string scope, string msg, int num) +// { +// ALog.Log($"{this.num} {scope} {msg} {num}"); +// } - private string GetCurrentTestName() - { - return $"[{TestContext.CurrentContext.Test.Name}]"; - } - } +// private string GetCurrentTestName() +// { +// return $"[{TestContext.CurrentContext.Test.Name}]"; +// } +// } - public class ALog - { - private static readonly object _lock = new object(); +// public class ALog +// { +// private static readonly object _lock = new object(); - public static void Log(string msg) - { - lock (_lock) - { - File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); - } - } - } +// public static void Log(string msg) +// { +// lock (_lock) +// { +// File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); +// } +// } +// } @@ -122,105 +122,99 @@ namespace FrameworkTests - public interface ITestLifecycleComponent - { - void Start(); - void Stop(string results); - } +// public class Base +// { +// private readonly Dictionary> anyFields = new(); - public class Base - { - private readonly Dictionary> anyFields = new(); +// public void Setup() +// { +// var testId = 23; - public void Setup() - { - var testId = 23; +// var fields = new Dictionary(); +// anyFields.Add(testId, fields); +// YieldFields(field => +// { +// fields.Add(field.GetType(), field); +// }); - var fields = new Dictionary(); - anyFields.Add(testId, fields); - YieldFields(field => - { - fields.Add(field.GetType(), field); - }); +// foreach (var field in fields.Values) +// { +// field.Start(); +// } +// } - foreach (var field in fields.Values) - { - field.Start(); - } - } +// public void TearDown() +// { +// var testId = 23; - public void TearDown() - { - var testId = 23; +// // foreach stop - // foreach stop +// anyFields.Remove(testId); +// } - anyFields.Remove(testId); - } +// public T Get() +// { +// int testId = 123; +// var fields = anyFields[testId]; +// var type = typeof(T); +// var result = fields[type]; +// return (T)result; +// } - public T Get() - { - int testId = 123; - var fields = anyFields[testId]; - var type = typeof(T); - var result = fields[type]; - return (T)result; - } +// public BaseFields GetBaseField() +// { +// return Get(); +// } - public BaseFields GetBaseField() - { - return Get(); - } +// protected virtual void YieldFields(Action giveField) +// { +// giveField(new BaseFields()); +// } +// } - protected virtual void YieldFields(Action giveField) - { - giveField(new BaseFields()); - } - } +// public class Mid : Base +// { +// protected override void YieldFields(Action giveField) +// { +// base.YieldFields(giveField); +// giveField(new MidFields()); +// } - public class Mid : Base - { - protected override void YieldFields(Action giveField) - { - base.YieldFields(giveField); - giveField(new MidFields()); - } +// public MidFields GetMid() +// { +// return Get(); +// } +// } - public MidFields GetMid() - { - return Get(); - } - } +// public class Top : Mid +// { +// protected override void YieldFields(Action giveField) +// { +// base.YieldFields(giveField); +// giveField(new TopFields()); +// } - public class Top : Mid - { - protected override void YieldFields(Action giveField) - { - base.YieldFields(giveField); - giveField(new TopFields()); - } +// public TopFields GetTop() +// { +// return Get(); +// } +// } - public TopFields GetTop() - { - return Get(); - } - } +// public class BaseFields : ITestLifecycleComponent +// { +// public string EntryPoint { get; set; } = string.Empty; +// public string Log { get; set; } = string.Empty; +// } - public class BaseFields : ITestLifecycleComponent - { - public string EntryPoint { get; set; } = string.Empty; - public string Log { get; set; } = string.Empty; - } +// public class MidFields : ITestLifecycleComponent +// { +// public string Nodes { get; set; } = string.Empty; +// } - public class MidFields : ITestLifecycleComponent - { - public string Nodes { get; set; } = string.Empty; - } - - public class TopFields : ITestLifecycleComponent - { - public string Geth { get; set; } = string.Empty; - public string Contracts { get; set; } = string.Empty; - } -} +// public class TopFields : ITestLifecycleComponent +// { +// public string Geth { get; set; } = string.Empty; +// public string Contracts { get; set; } = string.Empty; +// } +//} diff --git a/Tests/FrameworkTests/Parallelism.cs b/Tests/FrameworkTests/Parallelism.cs new file mode 100644 index 00000000..8a877f41 --- /dev/null +++ b/Tests/FrameworkTests/Parallelism.cs @@ -0,0 +1,6 @@ +using NUnit.Framework; + +[assembly: LevelOfParallelism(100)] +namespace FrameworkTests +{ +} From a1c5736e1234e6f200a3273e53f14aee04d108ab Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 23 Apr 2025 16:16:05 +0200 Subject: [PATCH 07/69] Log messages for successful asserts --- .../MarketTests/MarketplaceAutoBootstrapDistTest.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index a5ec0885..e5ad35a4 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -194,6 +194,8 @@ namespace CodexReleaseTests.MarketTests var expectedBalance = StartingBalanceTST.Tst() - GetContractFinalCost(pricePerBytePerSecond, contract, hosts); AssertTstBalance(client, expectedBalance, "Client balance incorrect."); + + Log($"Client has paid for contract. Balance: {expectedBalance}"); } protected void AssertHostsWerePaidForContract(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts) @@ -213,7 +215,9 @@ namespace CodexReleaseTests.MarketTests foreach (var pair in expectedBalances) { - AssertTstBalance(pair.Key, pair.Value, "Host was not paid for storage."); + AssertTstBalance(pair.Key, pair.Value, $"Host {pair.Key} was not paid for storage."); + + Log($"Host {pair.Key} was paid for storage. Balance: {pair.Value}"); } } From 3bb9a290544bda646a7f12828673a22ec2624a3d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 24 Apr 2025 12:53:08 +0200 Subject: [PATCH 08/69] sets up multiple successfulcontract tests --- Framework/KubernetesWorkflow/LogHandler.cs | 3 + .../ChainMonitor/ChainState.cs | 4 +- .../ChainMonitor/PeriodMonitor.cs | 11 ++- .../MarketTests/ChainMonitor.cs | 69 +++++++++++++++++++ .../MarketTests/ContractSuccessfulTest.cs | 30 +++++--- .../MarketplaceAutoBootstrapDistTest.cs | 21 +++++- .../MarketTests/MultipleContractsTest.cs | 44 ++++++------ Tests/DistTestCore/DistTest.cs | 2 +- Tests/DistTestCore/TestLifecycle.cs | 6 +- 9 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs diff --git a/Framework/KubernetesWorkflow/LogHandler.cs b/Framework/KubernetesWorkflow/LogHandler.cs index af77ef75..44adcc05 100644 --- a/Framework/KubernetesWorkflow/LogHandler.cs +++ b/Framework/KubernetesWorkflow/LogHandler.cs @@ -40,6 +40,9 @@ namespace KubernetesWorkflow protected override void ProcessLine(string line) { + if (line.Contains("Received JSON-RPC response")) return; + if (line.Contains("object field not marked with serialize, skipping")) return; + LogFile.WriteRaw(line); } } diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs index 25a028dc..32dfc5d0 100644 --- a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs @@ -47,7 +47,7 @@ namespace CodexContractsPlugin.ChainMonitor handler = changeHandler; this.doProofPeriodMonitoring = doProofPeriodMonitoring; TotalSpan = new TimeRange(startUtc, startUtc); - PeriodMonitor = new PeriodMonitor(this.log, contracts); + PeriodMonitor = new PeriodMonitor(contracts); } public TimeRange TotalSpan { get; private set; } @@ -78,7 +78,7 @@ namespace CodexContractsPlugin.ChainMonitor throw new Exception(msg); } - log.Log($"ChainState updating: {events.BlockInterval} = {events.All.Length} events."); + log.Debug($"ChainState updating: {events.BlockInterval} = {events.All.Length} events."); // Run through each block and apply the events to the state in order. var span = events.BlockInterval.TimeRange.Duration; diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/PeriodMonitor.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/PeriodMonitor.cs index 538ae124..2fbb5ff3 100644 --- a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/PeriodMonitor.cs +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/PeriodMonitor.cs @@ -1,18 +1,15 @@ -using Logging; -using Utils; +using Utils; namespace CodexContractsPlugin.ChainMonitor { public class PeriodMonitor { - private readonly ILog log; private readonly ICodexContracts contracts; private readonly List reports = new List(); private ulong? currentPeriod = null; - public PeriodMonitor(ILog log, ICodexContracts contracts) + public PeriodMonitor(ICodexContracts contracts) { - this.log = log; this.contracts = contracts; } @@ -39,8 +36,6 @@ namespace CodexContractsPlugin.ChainMonitor private void CreateReportForPeriod(ulong lastBlockInPeriod, ulong periodNumber, IChainStateRequest[] requests) { - log.Log("Creating report for period " + periodNumber); - ulong total = 0; ulong required = 0; var missed = new List(); @@ -87,6 +82,8 @@ namespace CodexContractsPlugin.ChainMonitor private void CalcStats() { IsEmpty = Reports.All(r => r.TotalProofsRequired == 0); + if (Reports.Length == 0) return; + PeriodLow = Reports.Min(r => r.PeriodNumber); PeriodHigh = Reports.Max(r => r.PeriodNumber); AverageNumSlots = Reports.Average(r => Convert.ToSingle(r.TotalNumSlots)); diff --git a/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs new file mode 100644 index 00000000..74f6a96a --- /dev/null +++ b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs @@ -0,0 +1,69 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; +using Logging; + +namespace CodexReleaseTests.MarketTests +{ + public class ChainMonitor + { + private readonly ILog log; + private readonly ICodexContracts contracts; + private readonly DateTime startUtc; + private readonly TimeSpan updateInterval; + private CancellationTokenSource cts = new CancellationTokenSource(); + private Task worker = Task.CompletedTask; + + public ChainMonitor(ILog log, ICodexContracts contracts, DateTime startUtc) + : this(log, contracts, startUtc, TimeSpan.FromSeconds(3.0)) + { + } + + public ChainMonitor(ILog log, ICodexContracts contracts, DateTime startUtc, TimeSpan updateInterval) + { + this.log = log; + this.contracts = contracts; + this.startUtc = startUtc; + this.updateInterval = updateInterval; + } + + public void Start() + { + cts = new CancellationTokenSource(); + worker = Task.Run(Worker); + } + + public void Stop() + { + cts.Cancel(); + worker.Wait(); + if (worker.Exception != null) throw worker.Exception; + } + + private void Worker() + { + var state = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, doProofPeriodMonitoring: true); + Thread.Sleep(updateInterval); + + while (!cts.IsCancellationRequested) + { + UpdateChainState(state); + + cts.Token.WaitHandle.WaitOne(updateInterval); + } + } + + private void UpdateChainState(ChainState state) + { + state.Update(); + + var reports = state.PeriodMonitor.GetAndClearReports(); + if (reports.IsEmpty) return; + + var slots = reports.Reports.Sum(r => Convert.ToInt32(r.TotalNumSlots)); + var required = reports.Reports.Sum(r => Convert.ToInt32(r.TotalProofsRequired)); + var missed = reports.Reports.Sum(r => Convert.ToInt32(r.MissedProofs)); + + log.Log($"Proof report: Slots={slots} Required={required} Missed={missed}"); + } + } +} diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 8431d121..66fd6542 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -4,16 +4,31 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture] + [TestFixture(6, 3, 1)] + [TestFixture(6, 4, 2)] + [TestFixture(8, 5, 2)] + [TestFixture(8, 6, 3)] + [TestFixture(12, 8, 1)] + [TestFixture(12, 8, 4)] public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest { - private const int FilesizeMb = 10; + public ContractSuccessfulTest(int hosts, int slots, int tolerance) + { + this.hosts = hosts; + this.slots = slots; + this.tolerance = tolerance; + } - protected override int NumberOfHosts => 6; + private const int FilesizeMb = 10; + private readonly TestToken pricePerBytePerSecond = 10.TstWei(); + private readonly int hosts; + private readonly int slots; + private readonly int tolerance; + + protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 1; protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration(); - private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] public void ContractSuccessful() @@ -44,11 +59,8 @@ namespace CodexReleaseTests.MarketTests { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - // TODO: this should work with NumberOfHosts, but - // an ongoing issue makes hosts sometimes not pick up slots. - // When it's resolved, we can reduce the number of hosts and slim down this test. - MinRequiredNumberOfNodes = 3, - NodeFailureTolerance = 1, + MinRequiredNumberOfNodes = (uint)slots, + NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 20, CollateralPerByte = 100.TstWei() diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index e5ad35a4..b0db0702 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -5,6 +5,7 @@ using CodexPlugin; using CodexTests; using DistTestCore; using GethPlugin; +using Logging; using Nethereum.Hex.HexConvertors.Extensions; using Utils; @@ -21,11 +22,15 @@ namespace CodexReleaseTests.MarketTests base.LifecycleStart(lifecycle); var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); - handles.Add(lifecycle, new MarketplaceHandle(geth, contracts)); + var monitor = SetupChainMonitor(lifecycle.Log, contracts, lifecycle.TestStartUtc); + handles.Add(lifecycle, new MarketplaceHandle(geth, contracts, monitor)); } protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) { + var handle = handles[lifecycle]; + if (handle.ChainMonitor != null) handle.ChainMonitor.Stop(); + handles.Remove(lifecycle); base.LifecycleStop(lifecycle, result); } @@ -50,6 +55,7 @@ namespace CodexReleaseTests.MarketTests protected abstract int NumberOfClients { get; } protected abstract ByteSize HostAvailabilitySize { get; } protected abstract TimeSpan HostAvailabilityMaxDuration { get; } + protected virtual bool MonitorChainState { get; } = true; public ICodexNodeGroup StartHosts() { @@ -112,6 +118,15 @@ namespace CodexReleaseTests.MarketTests }); } + private ChainMonitor? SetupChainMonitor(ILog log, ICodexContracts contracts, DateTime startUtc) + { + if (!MonitorChainState) return null; + + var result = new ChainMonitor(log, contracts, startUtc); + result.Start(); + return result; + } + private Retry GetBalanceAssertRetry() { return new Retry("AssertBalance", @@ -330,14 +345,16 @@ namespace CodexReleaseTests.MarketTests private class MarketplaceHandle { - public MarketplaceHandle(IGethNode geth, ICodexContracts contracts) + public MarketplaceHandle(IGethNode geth, ICodexContracts contracts, ChainMonitor? chainMonitor) { Geth = geth; Contracts = contracts; + ChainMonitor = chainMonitor; } public IGethNode Geth { get; } public ICodexContracts Contracts { get; } + public ChainMonitor? ChainMonitor { get; } } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 6ad10643..d91dec64 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -1,4 +1,5 @@ using CodexClient; +using CodexPlugin; using NUnit.Framework; using Utils; @@ -16,13 +17,25 @@ namespace CodexReleaseTests.MarketTests private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] - [Ignore("TODO - Test where multiple successful contracts are run simultaenously")] - public void MultipleSuccessfulContracts() + [Ignore("TODO - wip")] + [Combinatorial] + public void MultipleContractGenerations( + [Values(1, 5, 10)] int numGenerations) { var hosts = StartHosts(); + + for (var i = 0; i < numGenerations; i++) + { + Log("Generation: " + i); + Generation(hosts); + } + } + + private void Generation(ICodexNodeGroup hosts) + { var clients = StartClients(); - var requests = clients.Select(c => CreateStorageRequest(c)).ToArray(); + var requests = clients.Select(CreateStorageRequest).ToArray(); All(requests, r => { @@ -32,26 +45,17 @@ namespace CodexReleaseTests.MarketTests All(requests, r => r.WaitForStorageContractStarted()); All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); - All(requests, r => r.WaitForStorageContractFinished()); - - // todo: removed from codexclient: - //contracts.WaitUntilNextPeriod(); - //contracts.WaitUntilNextPeriod(); - - //var blocks = 3; - //Log($"Waiting {blocks} blocks for nodes to process payouts..."); - //Thread.Sleep(GethContainerRecipe.BlockInterval * blocks); - - // todo: - //AssertClientHasPaidForContract(pricePerSlotPerSecond, client, request, hosts); - //AssertHostsWerePaidForContract(pricePerSlotPerSecond, request, hosts); - //AssertHostsCollateralsAreUnchanged(hosts); } private void All(IStoragePurchaseContract[] requests, Action action) { - foreach (var r in requests) action(r); + var tasks = requests.Select(r => Task.Run(() => action(r))).ToArray(); + Task.WaitAll(tasks); + foreach(var t in tasks) + { + if (t.Exception != null) throw t.Exception; + } } private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) @@ -62,8 +66,8 @@ namespace CodexReleaseTests.MarketTests { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = (uint)NumberOfHosts, - NodeFailureTolerance = (uint)(NumberOfHosts / 2), + MinRequiredNumberOfNodes = (uint)NumberOfHosts / 2, + NodeFailureTolerance = (uint)(NumberOfHosts / 4), PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 20, CollateralPerByte = 1.Tst() diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 534eb74a..104bf8d8 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -164,7 +164,7 @@ namespace DistTestCore protected TimeRange GetTestRunTimeRange() { - return new TimeRange(Get().TestStart, DateTime.UtcNow); + return new TimeRange(Get().TestStartUtc, DateTime.UtcNow); } protected virtual void Initialize(FixtureLog fixtureLog) diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 3d642d20..53d0f177 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -26,7 +26,7 @@ namespace DistTestCore WebCallTimeSet = webCallTimeSet; K8STimeSet = k8sTimeSet; TestNamespace = testNamespace; - TestStart = DateTime.UtcNow; + TestStartUtc = DateTime.UtcNow; entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(k8sTimeSet, this, testNamespace), configuration.GetFileManagerFolder(), webCallTimeSet, k8sTimeSet); metadata = entryPoint.GetPluginMetadata(); @@ -36,7 +36,7 @@ namespace DistTestCore log.WriteLogTag(); } - public DateTime TestStart { get; } + public DateTime TestStartUtc { get; } public TestLog Log { get; } public Configuration Configuration { get; } public IWebCallTimeSet WebCallTimeSet { get; } @@ -76,7 +76,7 @@ namespace DistTestCore public TimeSpan GetTestDuration() { - return DateTime.UtcNow - TestStart; + return DateTime.UtcNow - TestStartUtc; } public void OnContainersStarted(RunningPod rc) From 9a52b217652ebf3c331645ba569030824d2f5056 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 24 Apr 2025 15:34:31 +0200 Subject: [PATCH 09/69] extended multigeneration contract testing --- .../MarketTests/ContractSuccessfulTest.cs | 9 +++- .../MarketTests/MultipleContractsTest.cs | 51 ++++++++++++++----- .../NodeTests/BasicInfoTests.cs | 8 +-- Tests/CodexReleaseTests/Parallelism.cs | 2 +- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 66fd6542..7750ede1 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -6,10 +6,15 @@ namespace CodexReleaseTests.MarketTests { [TestFixture(6, 3, 1)] [TestFixture(6, 4, 2)] + [TestFixture(8, 5, 1)] [TestFixture(8, 5, 2)] + [TestFixture(8, 6, 1)] + [TestFixture(8, 6, 2)] [TestFixture(8, 6, 3)] - [TestFixture(12, 8, 1)] - [TestFixture(12, 8, 4)] + [TestFixture(8, 8, 1)] + [TestFixture(8, 8, 2)] + [TestFixture(8, 8, 3)] + [TestFixture(8, 8, 4)] public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest { public ContractSuccessfulTest(int hosts, int slots, int tolerance) diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index d91dec64..815c4697 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -5,22 +5,42 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture] + [TestFixture(6, 3, 1)] + [TestFixture(6, 4, 1)] + [TestFixture(6, 4, 2)] + [TestFixture(8, 5, 1)] + [TestFixture(8, 5, 2)] + [TestFixture(8, 6, 1)] + [TestFixture(8, 6, 2)] + [TestFixture(8, 6, 3)] + [TestFixture(8, 8, 1)] + [TestFixture(8, 8, 2)] + [TestFixture(8, 8, 3)] + [TestFixture(8, 8, 4)] public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest { - private const int FilesizeMb = 10; + public MultipleContractsTest(int hosts, int slots, int tolerance) + { + this.hosts = hosts; + this.slots = slots; + this.tolerance = tolerance; + } - protected override int NumberOfHosts => 8; + private const int FilesizeMb = 10; + private readonly int hosts; + private readonly int slots; + private readonly int tolerance; + + protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 3; - protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); - protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration(); + protected override ByteSize HostAvailabilitySize => (100 * FilesizeMb).MB(); + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 3; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] - [Ignore("TODO - wip")] [Combinatorial] public void MultipleContractGenerations( - [Values(1, 5, 10)] int numGenerations) + [Values(5)] int numGenerations) { var hosts = StartHosts(); @@ -44,8 +64,13 @@ namespace CodexReleaseTests.MarketTests }); All(requests, r => r.WaitForStorageContractStarted()); - All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); - All(requests, r => r.WaitForStorageContractFinished()); + + Thread.Sleep(TimeSpan.FromSeconds(12.0)); + clients.Stop(waitTillStopped: false); + + // for the time being, we're only interested in whether these contracts start. + //All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); + //All(requests, r => r.WaitForStorageContractFinished()); } private void All(IStoragePurchaseContract[] requests, Action action) @@ -66,11 +91,11 @@ namespace CodexReleaseTests.MarketTests { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = (uint)NumberOfHosts / 2, - NodeFailureTolerance = (uint)(NumberOfHosts / 4), + MinRequiredNumberOfNodes = (uint)slots, + NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 20, - CollateralPerByte = 1.Tst() + CollateralPerByte = 1.TstWei() }); } @@ -81,7 +106,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() / 2; + return Get8TimesConfiguredPeriodDuration(); } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs b/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs index 9b9f4bbf..f6cba228 100644 --- a/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs +++ b/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs @@ -1,11 +1,5 @@ -using CodexPlugin; -using CodexTests; +using CodexTests; using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Utils; namespace CodexReleaseTests.NodeTests diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index a1b26c73..ab6a11fb 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(1)] +[assembly: LevelOfParallelism(3)] namespace CodexReleaseTests.DataTests { } From 5d61097838945901e3576244bcad16ab8ba510ed Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 24 Apr 2025 19:33:34 +0200 Subject: [PATCH 10/69] wip --- ProjectPlugins/CodexPlugin/CodexDockerImage.cs | 3 ++- .../MarketTests/ContractSuccessfulTest.cs | 7 +++++-- .../MarketTests/MultipleContractsTest.cs | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index bbb09c0b..ec8d6395 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,7 +2,8 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-97e9684-dist-tests"; + //"codexstorage/nim-codex:0.2.1-dist-tests"; public static string Override { get; set; } = string.Empty; diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 7750ede1..9aa22eab 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -33,7 +33,7 @@ namespace CodexReleaseTests.MarketTests protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 1; protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); - protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration(); + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] public void ContractSuccessful() @@ -49,6 +49,9 @@ namespace CodexReleaseTests.MarketTests request.WaitForStorageContractStarted(); AssertContractSlotsAreFilledByHosts(request, hosts); + Thread.Sleep(TimeSpan.FromSeconds(12.0)); + return; + request.WaitForStorageContractFinished(); AssertClientHasPaidForContract(pricePerBytePerSecond, client, request, hosts); @@ -79,7 +82,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() / 2; + return Get8TimesConfiguredPeriodDuration() * 4; } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 815c4697..69fe413b 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -34,13 +34,13 @@ namespace CodexReleaseTests.MarketTests protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 3; protected override ByteSize HostAvailabilitySize => (100 * FilesizeMb).MB(); - protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 3; + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] [Combinatorial] public void MultipleContractGenerations( - [Values(5)] int numGenerations) + [Values(5, 10)] int numGenerations) { var hosts = StartHosts(); @@ -49,6 +49,8 @@ namespace CodexReleaseTests.MarketTests Log("Generation: " + i); Generation(hosts); } + + Thread.Sleep(TimeSpan.FromSeconds(12.0)); } private void Generation(ICodexNodeGroup hosts) @@ -65,7 +67,6 @@ namespace CodexReleaseTests.MarketTests All(requests, r => r.WaitForStorageContractStarted()); - Thread.Sleep(TimeSpan.FromSeconds(12.0)); clients.Stop(waitTillStopped: false); // for the time being, we're only interested in whether these contracts start. @@ -106,7 +107,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration(); + return Get8TimesConfiguredPeriodDuration() * 4; } private TimeSpan Get8TimesConfiguredPeriodDuration() From 373f7826bf5c87d6b53cb7b0d8478bded7ab7ff9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 25 Apr 2025 07:21:07 +0200 Subject: [PATCH 11/69] todo: try fixture lifecycle attribute --- Tests/FrameworkTests/LifecycelyTest.cs | 329 ++++++++++++------------- 1 file changed, 164 insertions(+), 165 deletions(-) diff --git a/Tests/FrameworkTests/LifecycelyTest.cs b/Tests/FrameworkTests/LifecycelyTest.cs index 6c11e14f..e420a911 100644 --- a/Tests/FrameworkTests/LifecycelyTest.cs +++ b/Tests/FrameworkTests/LifecycelyTest.cs @@ -1,64 +1,65 @@ -//using NUnit.Framework; +using NUnit.Framework; -//namespace FrameworkTests -//{ -// [Parallelizable(ParallelScope.All)] -// [TestFixture(10)] -// [TestFixture(20)] -// [TestFixture(30)] -// public class LifecycelyTest -// { -// public LifecycelyTest(int num) -// { -// Log("ctor", GetCurrentTestName(), num); -// this.num = num; -// } +namespace FrameworkTests +{ + [Parallelizable(ParallelScope.All)] + [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] + [TestFixture(10)] + [TestFixture(20)] + [TestFixture(30)] + public class LifecycelyTest + { + public LifecycelyTest(int num) + { + Log("ctor", GetCurrentTestName(), num); + this.num = num; + } -// [SetUp] -// public void Setup() -// { -// Log(nameof(Setup), GetCurrentTestName()); -// } + [SetUp] + public void Setup() + { + Log(nameof(Setup), GetCurrentTestName()); + } -// [TearDown] -// public void TearDown() -// { -// Log(nameof(TearDown), GetCurrentTestName()); -// } + [TearDown] + public void TearDown() + { + Log(nameof(TearDown), GetCurrentTestName()); + } -// [Test] -// public void A() -// { -// Log(nameof(A), "Run"); -// SleepRandom(); -// Log(nameof(A), "Finish"); -// } + //[Test] + //public void A() + //{ + // Log(nameof(A), "Run"); + // SleepRandom(); + // Log(nameof(A), "Finish"); + //} -// [Test] -// public void B() -// { -// Log(nameof(B), "Run"); -// SleepRandom(); -// Log(nameof(B), "Finish"); -// } + //[Test] + //public void B() + //{ + // Log(nameof(B), "Run"); + // SleepRandom(); + // Log(nameof(B), "Finish"); + //} -// [Test] -// public void C() -// { -// Log(nameof(C), "Run"); -// SleepRandom(); -// Log(nameof(C), "Finish"); -// } + //[Test] + //public void C() + //{ + // Log(nameof(C), "Run"); + // SleepRandom(); + // Log(nameof(C), "Finish"); + //} -// [Test] -// [Combinatorial] -// public void Multi( -// [Values(1, 2, 3)] int num) -// { -// Log(nameof(Multi), "Run", num); -// SleepRandom(); -// Log(nameof(Multi), "Finish", num); -// } + [Test] + [Combinatorial] + public void Multi( + [Values(1, 2, 3)] int num) + { + Log(nameof(Multi), "Run", num); + SleepRandom(); + Log(nameof(Multi), "Finish", num); + } @@ -71,150 +72,148 @@ -// private static readonly Random r = new Random(); -// private readonly int num; + private static readonly Random r = new Random(); + private readonly int num; -// private void SleepRandom() -// { -// Thread.Sleep(TimeSpan.FromSeconds(5.0)); -// Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); -// } + private void SleepRandom() + { + Thread.Sleep(TimeSpan.FromSeconds(5.0)); + Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); + } -// private void Log(string scope, string msg) -// { -// ALog.Log($"{num} {scope} {msg}"); -// } + private void Log(string scope, string msg) + { + ALog.Log($"{num} {scope} {msg}"); + } -// private void Log(string scope, string msg, int num) -// { -// ALog.Log($"{this.num} {scope} {msg} {num}"); -// } + private void Log(string scope, string msg, int num) + { + ALog.Log($"{this.num} {scope} {msg} {num}"); + } -// private string GetCurrentTestName() -// { -// return $"[{TestContext.CurrentContext.Test.Name}]"; -// } -// } + private string GetCurrentTestName() + { + return $"[{TestContext.CurrentContext.Test.Name}]"; + } + } -// public class ALog -// { -// private static readonly object _lock = new object(); + public class ALog + { + private static readonly object _lock = new object(); -// public static void Log(string msg) -// { -// lock (_lock) -// { -// File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); -// } -// } -// } + public static void Log(string msg) + { + lock (_lock) + { + File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); + } + } + } + public interface ITestLifecycleComponent + { + } + public class Base + { + private readonly Dictionary> anyFields = new(); -// public class Base -// { -// private readonly Dictionary> anyFields = new(); + public void Setup() + { + var testId = 23; -// public void Setup() -// { -// var testId = 23; + var fields = new Dictionary(); + anyFields.Add(testId, fields); + YieldFields(field => + { + fields.Add(field.GetType(), field); + }); -// var fields = new Dictionary(); -// anyFields.Add(testId, fields); -// YieldFields(field => -// { -// fields.Add(field.GetType(), field); -// }); + } -// foreach (var field in fields.Values) -// { -// field.Start(); -// } -// } + public void TearDown() + { + var testId = 23; -// public void TearDown() -// { -// var testId = 23; + // foreach stop -// // foreach stop + anyFields.Remove(testId); + } -// anyFields.Remove(testId); -// } + public T Get() + { + int testId = 123; + var fields = anyFields[testId]; + var type = typeof(T); + var result = fields[type]; + return (T)result; + } -// public T Get() -// { -// int testId = 123; -// var fields = anyFields[testId]; -// var type = typeof(T); -// var result = fields[type]; -// return (T)result; -// } + public BaseFields GetBaseField() + { + return Get(); + } -// public BaseFields GetBaseField() -// { -// return Get(); -// } + protected virtual void YieldFields(Action giveField) + { + giveField(new BaseFields()); + } + } -// protected virtual void YieldFields(Action giveField) -// { -// giveField(new BaseFields()); -// } -// } + public class Mid : Base + { + protected override void YieldFields(Action giveField) + { + base.YieldFields(giveField); + giveField(new MidFields()); + } -// public class Mid : Base -// { -// protected override void YieldFields(Action giveField) -// { -// base.YieldFields(giveField); -// giveField(new MidFields()); -// } + public MidFields GetMid() + { + return Get(); + } + } -// public MidFields GetMid() -// { -// return Get(); -// } -// } + public class Top : Mid + { + protected override void YieldFields(Action giveField) + { + base.YieldFields(giveField); + giveField(new TopFields()); + } -// public class Top : Mid -// { -// protected override void YieldFields(Action giveField) -// { -// base.YieldFields(giveField); -// giveField(new TopFields()); -// } + public TopFields GetTop() + { + return Get(); + } + } -// public TopFields GetTop() -// { -// return Get(); -// } -// } + public class BaseFields : ITestLifecycleComponent + { + public string EntryPoint { get; set; } = string.Empty; + public string Log { get; set; } = string.Empty; + } -// public class BaseFields : ITestLifecycleComponent -// { -// public string EntryPoint { get; set; } = string.Empty; -// public string Log { get; set; } = string.Empty; -// } + public class MidFields : ITestLifecycleComponent + { + public string Nodes { get; set; } = string.Empty; + } -// public class MidFields : ITestLifecycleComponent -// { -// public string Nodes { get; set; } = string.Empty; -// } - -// public class TopFields : ITestLifecycleComponent -// { -// public string Geth { get; set; } = string.Empty; -// public string Contracts { get; set; } = string.Empty; -// } -//} + public class TopFields : ITestLifecycleComponent + { + public string Geth { get; set; } = string.Empty; + public string Contracts { get; set; } = string.Empty; + } +} From ad1b756db9c85f22e5a820bcb2fdc9e28661fb33 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 25 Apr 2025 11:08:38 +0200 Subject: [PATCH 12/69] Revert "while better the new plan is still a mess" This reverts commit 2dfdfac2bb52e4ff919ee5f1b3986a839d71b0a2. # Conflicts: # Tests/FrameworkTests/LifecycelyTest.cs --- .../OverwatchSupport/CodexTranscriptWriter.cs | 15 +- .../CodexTranscriptWriterConfig.cs | 4 +- .../MarketplaceAutoBootstrapDistTest.cs | 39 ++- Tests/DistTestCore/DistTest.cs | 124 +++++++--- .../DistTestLifecycleComponents.cs | 97 -------- Tests/DistTestCore/Logs/FixtureLog.cs | 13 +- Tests/DistTestCore/Logs/TestLog.cs | 2 + Tests/DistTestCore/TestLifecycle.cs | 30 +-- .../AutoBootstrapDistTest.cs | 62 ++--- Tests/ExperimentalTests/CodexDistTest.cs | 223 ++++++++++++------ .../CodexLogTrackerProvider.cs | 73 ------ Tests/FrameworkTests/Parallelism.cs | 6 - 12 files changed, 297 insertions(+), 391 deletions(-) delete mode 100644 Tests/DistTestCore/DistTestLifecycleComponents.cs delete mode 100644 Tests/ExperimentalTests/CodexLogTrackerProvider.cs delete mode 100644 Tests/FrameworkTests/Parallelism.cs diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs index 53ac1496..afd8d1a2 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs @@ -23,27 +23,16 @@ namespace CodexPlugin.OverwatchSupport converter = new CodexLogConverter(writer, config, identityMap); } - public void Finalize() + public void Finalize(string outputFilepath) { log.Log("Finalizing Codex transcript..."); writer.AddHeader(CodexHeaderKey, CreateCodexHeader()); - writer.Write(GetOutputFullPath()); + writer.Write(outputFilepath); log.Log("Done"); } - private string GetOutputFullPath() - { - var outputPath = Path.GetDirectoryName(log.GetFullName()); - if (outputPath == null) throw new Exception("Logfile path is null"); - var filename = Path.GetFileNameWithoutExtension(log.GetFullName()); - if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); - var outputFile = Path.Combine(outputPath, filename + "_" + config.OutputFilename); - if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; - return outputFile; - } - public ICodexNodeHooks CreateHooks(string nodeName) { nodeName = Str.Between(nodeName, "'", "'"); diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs index 8c5e7bf0..247494c8 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs @@ -2,13 +2,11 @@ { public class CodexTranscriptWriterConfig { - public CodexTranscriptWriterConfig(string outputFilename, bool includeBlockReceivedEvents) + public CodexTranscriptWriterConfig(bool includeBlockReceivedEvents) { - OutputFilename = outputFilename; IncludeBlockReceivedEvents = includeBlockReceivedEvents; } - public string OutputFilename { get; } public bool IncludeBlockReceivedEvents { get; } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index f6e8a9ca..a5ec0885 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -10,41 +10,18 @@ using Utils; namespace CodexReleaseTests.MarketTests { - public class MarketplaceTestComponent : ILifecycleComponent - { - public IGethNode Geth { get; } - public ICodexContracts Contracts { get; } - - public void Start(ILifecycleComponentAccess access) - { - throw new NotImplementedException(); - } - - public void Stop(ILifecycleComponentAccess access, DistTestResult result) - { - throw new NotImplementedException(); - } - } - public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest { + private readonly Dictionary handles = new Dictionary(); protected const int StartingBalanceTST = 1000; protected const int StartingBalanceEth = 10; - protected override void CreateComponents(ILifecycleComponentCollector collector) - { - base.CreateComponents(collector); - - - collector.AddComponent(new MarketplaceTestComponent()); - } - protected override void LifecycleStart(TestLifecycle lifecycle) { base.LifecycleStart(lifecycle); var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); - handles.Add(lifecycle, new MarketplaceTestComponent(geth, contracts)); + handles.Add(lifecycle, new MarketplaceHandle(geth, contracts)); } protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) @@ -346,5 +323,17 @@ namespace CodexReleaseTests.MarketTests public SlotFilledEventDTO SlotFilledEvent { get; } public ICodexNode Host { get; } } + + private class MarketplaceHandle + { + public MarketplaceHandle(IGethNode geth, ICodexContracts contracts) + { + Geth = geth; + Contracts = contracts; + } + + public IGethNode Geth { get; } + public ICodexContracts Contracts { get; } + } } } diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 9a969532..534eb74a 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -19,8 +19,9 @@ namespace DistTestCore private readonly Assembly[] testAssemblies; private readonly FixtureLog fixtureLog; private readonly StatusLog statusLog; + private readonly object lifecycleLock = new object(); private readonly EntryPoint globalEntryPoint; - private readonly DistTestLifecycleComponents lifecycleComponents = new DistTestLifecycleComponents(); + private readonly Dictionary lifecycles = new Dictionary(); private readonly string deployId; public DistTest() @@ -88,12 +89,7 @@ namespace DistTestCore { try { - var testName = GetCurrentTestName(); - fixtureLog.WriteLogTag(); - Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () => - { - lifecycleComponents.Setup(testName, CreateComponents); - }); + CreateNewTestLifecycle(); } catch (Exception ex) { @@ -108,7 +104,7 @@ namespace DistTestCore { try { - Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", DisposeTestLifecycle); + DisposeTestLifecycle(); } catch (Exception ex) { @@ -175,49 +171,80 @@ namespace DistTestCore { } - protected virtual void CreateComponents(ILifecycleComponentCollector collector) - { - var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); - var lifecycle = new TestLifecycle( - fixtureLog.CreateTestLog(), - configuration, - GetWebCallTimeSet(), - GetK8sTimeSet(), - testNamespace, - GetCurrentTestName(), - deployId, - ShouldWaitForCleanup()); - - collector.AddComponent(lifecycle); - } - - protected virtual void DestroyComponents(TestLifecycle lifecycle, DistTestResult testResult) + protected virtual void LifecycleStart(TestLifecycle lifecycle) { } - public T Get() where T : ILifecycleComponent + protected virtual void LifecycleStop(TestLifecycle lifecycle, DistTestResult testResult) { - return lifecycleComponents.Get(GetCurrentTestName()); } - private TestLifecycle Get() + protected virtual void CollectStatusLogData(TestLifecycle lifecycle, Dictionary data) { - return Get(); + } + + protected TestLifecycle Get() + { + lock (lifecycleLock) + { + return lifecycles[GetCurrentTestName()]; + } + } + + private void CreateNewTestLifecycle() + { + var testName = GetCurrentTestName(); + fixtureLog.WriteLogTag(); + Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () => + { + lock (lifecycleLock) + { + var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); + var lifecycle = new TestLifecycle( + fixtureLog.CreateTestLog(), + configuration, + GetWebCallTimeSet(), + GetK8sTimeSet(), + testNamespace, + deployId, + ShouldWaitForCleanup()); + lifecycles.Add(testName, lifecycle); + LifecycleStart(lifecycle); + } + }); } private void DisposeTestLifecycle() { - var testName = GetCurrentTestName(); - var results = GetTestResult(); var lifecycle = Get(); + var testResult = GetTestResult(); var testDuration = lifecycle.GetTestDuration(); var data = lifecycle.GetPluginMetadata(); - fixtureLog.Log($"{GetCurrentTestName()} = {results} ({testDuration})"); - statusLog.ConcludeTest(results, testDuration, data); + CollectStatusLogData(lifecycle, data); + fixtureLog.Log($"{GetCurrentTestName()} = {testResult} ({testDuration})"); + statusLog.ConcludeTest(testResult, testDuration, data); + Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", () => + { + WriteEndTestLog(lifecycle.Log); - lifecycleComponents.TearDown(testName, results); + IncludeLogsOnTestFailure(lifecycle); + LifecycleStop(lifecycle, testResult); + lifecycle.DeleteAllResources(); + lifecycles.Remove(GetCurrentTestName()); + }); } + private void WriteEndTestLog(TestLog log) + { + var result = TestContext.CurrentContext.Result; + + Log($"*** Finished: {GetCurrentTestName()} = {result.Outcome.Status}"); + if (!string.IsNullOrEmpty(result.Message)) + { + Log(result.Message); + Log($"{result.StackTrace}"); + } + } private IWebCallTimeSet GetWebCallTimeSet() { @@ -269,6 +296,28 @@ namespace DistTestCore .ToArray(); } + private void IncludeLogsOnTestFailure(TestLifecycle lifecycle) + { + var testStatus = TestContext.CurrentContext.Result.Outcome.Status; + if (ShouldDownloadAllLogs(testStatus)) + { + lifecycle.Log.Log("Downloading all container logs..."); + lifecycle.DownloadAllLogs(); + } + } + + private bool ShouldDownloadAllLogs(TestStatus testStatus) + { + if (configuration.AlwaysDownloadContainerLogs) return true; + if (!IsDownloadingLogsEnabled()) return false; + if (testStatus == TestStatus.Failed) + { + return true; + } + + return false; + } + private string GetCurrentTestName() { return $"[{TestContext.CurrentContext.Test.Name}]"; @@ -279,8 +328,7 @@ namespace DistTestCore var success = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed; var status = TestContext.CurrentContext.Result.Outcome.Status.ToString(); var result = TestContext.CurrentContext.Result.Message; - var trace = TestContext.CurrentContext.Result.StackTrace; - return new DistTestResult(success, status, result ?? string.Empty, trace ?? string.Empty); + return new DistTestResult(success, status, result ?? string.Empty); } private bool IsDownloadingLogsEnabled() @@ -291,18 +339,16 @@ namespace DistTestCore public class DistTestResult { - public DistTestResult(bool success, string status, string result, string trace) + public DistTestResult(bool success, string status, string result) { Success = success; Status = status; Result = result; - Trace = trace; } public bool Success { get; } public string Status { get; } public string Result { get; } - public string Trace { get; } public override string ToString() { diff --git a/Tests/DistTestCore/DistTestLifecycleComponents.cs b/Tests/DistTestCore/DistTestLifecycleComponents.cs deleted file mode 100644 index 203a891c..00000000 --- a/Tests/DistTestCore/DistTestLifecycleComponents.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace DistTestCore -{ - public interface ILifecycleComponent - { - void Start(ILifecycleComponentAccess access); - void Stop(ILifecycleComponentAccess access, DistTestResult result); - } - - public interface ILifecycleComponentCollector - { - void AddComponent(ILifecycleComponent component); - } - - public interface ILifecycleComponentAccess - { - T Get() where T : ILifecycleComponent; - } - - public class DistTestLifecycleComponents - { - private readonly object _lock = new object(); - private readonly Dictionary> components = new(); - - public void Setup(string testName, Action initializer) - { - var newComponents = new Dictionary(); - lock (_lock) - { - components.Add(testName, newComponents); - var collector = new Collector(newComponents); - initializer(collector); - } - - var access = new ScopedAccess(this, testName); - foreach (var component in newComponents.Values) - { - component.Start(access); - } - } - - public T Get(string testName) where T : ILifecycleComponent - { - var type = typeof(T); - lock (_lock) - { - return (T)components[testName][type]; - } - } - - public void TearDown(string testName, DistTestResult result) - { - var access = new ScopedAccess(this, testName); - var closingComponents = components[testName]; - foreach (var component in closingComponents.Values) - { - component.Stop(access, result); - } - - lock (_lock) - { - components.Remove(testName); - } - } - - private class Collector : ILifecycleComponentCollector - { - private readonly Dictionary components; - - public Collector(Dictionary components) - { - this.components = components; - } - - public void AddComponent(ILifecycleComponent component) - { - components.Add(component.GetType(), component); - } - } - - private class ScopedAccess : ILifecycleComponentAccess - { - private readonly DistTestLifecycleComponents parent; - private readonly string testName; - - public ScopedAccess(DistTestLifecycleComponents parent, string testName) - { - this.parent = parent; - this.testName = testName; - } - - public T Get() where T : ILifecycleComponent - { - return parent.Get(testName); - } - } - } -} diff --git a/Tests/DistTestCore/Logs/FixtureLog.cs b/Tests/DistTestCore/Logs/FixtureLog.cs index f90147d8..9d3c77d3 100644 --- a/Tests/DistTestCore/Logs/FixtureLog.cs +++ b/Tests/DistTestCore/Logs/FixtureLog.cs @@ -4,25 +4,26 @@ namespace DistTestCore.Logs { public class FixtureLog : BaseTestLog { + private readonly ILog backingLog; + private readonly string deployId; + public FixtureLog(ILog backingLog, string deployId) : base(backingLog, deployId) { + this.backingLog = backingLog; + this.deployId = deployId; } public TestLog CreateTestLog(string name = "") { - var result = TestLog.Create(this, name); - result.Log(NameUtils.GetRawFixtureName()); - return result; + return TestLog.Create(this, name); } public static FixtureLog Create(LogConfig config, DateTime start, string deployId, string name = "") { var fullName = NameUtils.GetFixtureFullName(config, start, name); var log = CreateMainLog(fullName, name); - var result = new FixtureLog(log, deployId); - result.Log(NameUtils.GetRawFixtureName()); - return result; + return new FixtureLog(log, deployId); } } } diff --git a/Tests/DistTestCore/Logs/TestLog.cs b/Tests/DistTestCore/Logs/TestLog.cs index 6a334138..0dca1464 100644 --- a/Tests/DistTestCore/Logs/TestLog.cs +++ b/Tests/DistTestCore/Logs/TestLog.cs @@ -1,4 +1,5 @@ using Logging; +using System.Xml.Linq; namespace DistTestCore.Logs { @@ -7,6 +8,7 @@ namespace DistTestCore.Logs public TestLog(ILog backingLog, string methodName, string deployId, string name = "") : base(backingLog, deployId) { + backingLog.Log($"*** Begin: {methodName}"); } public static TestLog Create(FixtureLog parentLog, string name = "") diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 550b6151..3d642d20 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -10,24 +10,22 @@ using WebUtils; namespace DistTestCore { - public class TestLifecycle : IK8sHooks, ILifecycleComponent + public class TestLifecycle : IK8sHooks { private const string TestsType = "dist-tests"; private readonly EntryPoint entryPoint; private readonly Dictionary metadata; private readonly List runningContainers = new(); - private readonly string testName; private readonly string deployId; private readonly List stoppedContainerLogs = new List(); - public TestLifecycle(TestLog log, Configuration configuration, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8sTimeSet, string testNamespace, string testName, string deployId, bool waitForCleanup) + public TestLifecycle(TestLog log, Configuration configuration, IWebCallTimeSet webCallTimeSet, IK8sTimeSet k8sTimeSet, string testNamespace, string deployId, bool waitForCleanup) { Log = log; Configuration = configuration; WebCallTimeSet = webCallTimeSet; K8STimeSet = k8sTimeSet; TestNamespace = testNamespace; - this.testName = testName; TestStart = DateTime.UtcNow; entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(k8sTimeSet, this, testNamespace), configuration.GetFileManagerFolder(), webCallTimeSet, k8sTimeSet); @@ -38,28 +36,6 @@ namespace DistTestCore log.WriteLogTag(); } - public void Start(ILifecycleComponentAccess access) - { - Log.Log($"*** Begin: {testName}"); - } - - public void Stop(ILifecycleComponentAccess access, DistTestResult result) - { - Log.Log($"*** Finished: {testName} = {result.Status}"); - if (!string.IsNullOrEmpty(result.Result)) - { - Log.Log(result.Result); - Log.Log($"{result.Trace}"); - } - - if (!result.Success) - { - DownloadAllLogs(); - } - - DeleteAllResources(); - } - public DateTime TestStart { get; } public TestLog Log { get; } public Configuration Configuration { get; } @@ -69,7 +45,7 @@ namespace DistTestCore public bool WaitForCleanup { get; } public CoreInterface CoreInterface { get; } - private void DeleteAllResources() + public void DeleteAllResources() { entryPoint.Decommission( deleteKubernetesResources: true, diff --git a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs index a2477597..d8ac6805 100644 --- a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs +++ b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs @@ -4,56 +4,46 @@ using DistTestCore; namespace CodexTests { - public class AutoBootstrapComponent : ILifecycleComponent - { - public ICodexNode? BootstrapNode { get; private set; } = null; - - public void Start(ILifecycleComponentAccess access) - { - if (BootstrapNode != null) return; - - var tl = access.Get(); - var ci = tl.CoreInterface; - var testNamespace = tl.TestNamespace; - - BootstrapNode = ci.StartCodexNode(s => s.WithName("BOOTSTRAP_" + testNamespace)); - } - - public void ApplyBootstrapNode(ICodexSetup setup) - { - if (BootstrapNode == null) return; - - setup.WithBootstrapNode(BootstrapNode); - } - - public void Stop(ILifecycleComponentAccess access, DistTestResult result) - { - if (BootstrapNode == null) return; - BootstrapNode.Stop(waitTillStopped: false); - } - } - public class AutoBootstrapDistTest : CodexDistTest { + private readonly Dictionary bootstrapNodes = new Dictionary(); + private bool isBooting = false; - protected override void CreateComponents(ILifecycleComponentCollector collector) + protected override void LifecycleStart(TestLifecycle tl) { - base.CreateComponents(collector); - collector.AddComponent(new AutoBootstrapComponent()); + base.LifecycleStart(tl); + if (!bootstrapNodes.ContainsKey(tl)) + { + isBooting = true; + bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace))); + isBooting = false; + } + } + + protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) + { + bootstrapNodes.Remove(lifecycle); + base.LifecycleStop(lifecycle, result); } protected override void OnCodexSetup(ICodexSetup setup) { - Get().ApplyBootstrapNode(setup); + if (isBooting) return; + + var node = BootstrapNode; + if (node != null) setup.WithBootstrapNode(node); } protected ICodexNode BootstrapNode { get { - var bn = Get().BootstrapNode; - if (bn == null) throw new InvalidOperationException("BootstrapNode accessed before initialized."); - return bn; + var tl = Get(); + if (bootstrapNodes.TryGetValue(tl, out var node)) + { + return node; + } + throw new InvalidOperationException("Bootstrap node not yet started."); } } } diff --git a/Tests/ExperimentalTests/CodexDistTest.cs b/Tests/ExperimentalTests/CodexDistTest.cs index 40e711fc..d27216d3 100644 --- a/Tests/ExperimentalTests/CodexDistTest.cs +++ b/Tests/ExperimentalTests/CodexDistTest.cs @@ -1,5 +1,6 @@ using BlockchainUtils; using CodexClient; +using CodexClient.Hooks; using CodexContractsPlugin; using CodexNetDeployer; using CodexPlugin; @@ -16,74 +17,86 @@ using Newtonsoft.Json; using NUnit.Framework; using NUnit.Framework.Constraints; using OverwatchTranscript; +using Utils; namespace CodexTests { - public class CodexDistTestComponents : ILifecycleComponent + public class CodexLogTrackerProvider : ICodexHooksProvider { - private readonly object nodesLock = new object(); + private readonly Action addNode; - public CodexDistTestComponents(CodexTranscriptWriter? writer) + public CodexLogTrackerProvider(Action addNode) { - Writer = writer; + this.addNode = addNode; } - public CodexTranscriptWriter? Writer { get; } - public BlockCache Cache { get; } = new(); - public List Nodes { get; } = new(); - - public void Start(ILifecycleComponentAccess access) + // See TestLifecycle.cs DownloadAllLogs() + public ICodexNodeHooks CreateHooks(string nodeName) { - var ci = access.Get().CoreInterface; - ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n => + return new CodexLogTracker(addNode); + } + + public class CodexLogTracker : ICodexNodeHooks + { + private readonly Action addNode; + + public CodexLogTracker(Action addNode) { - lock (nodesLock) - { - Nodes.Add(n); - } - })); - } - - public void Stop(ILifecycleComponentAccess access, DistTestResult result) - { - var tl = access.Get(); - var log = tl.Log; - var logFiles = tl.DownloadAllLogs(); - - TeardownTranscript(log, logFiles, result); - - // todo: on not success: go to nodes and dl logs? - // or fix disttest failure log download so we can always have logs even for non-codexes? - } - - private void TeardownTranscript(TestLog log, IDownloadedLog[] logFiles, DistTestResult result) - { - if (Writer == null) return; - - Writer.AddResult(result.Success, result.Result); - - try - { - Stopwatch.Measure(log, "Transcript.ProcessLogs", () => - { - Writer.ProcessLogs(logFiles); - }); - - Stopwatch.Measure(log, $"Transcript.Finalize", () => - { - Writer.IncludeFile(log.GetFullName()); - Writer.Finalize(); - }); + this.addNode = addNode; } - catch (Exception ex) + + public void OnFileDownloaded(ByteSize size, ContentId cid) + { + } + + public void OnFileDownloading(ContentId cid) + { + } + + public void OnFileUploaded(string uid, ByteSize size, ContentId cid) + { + } + + public void OnFileUploading(string uid, ByteSize size) + { + } + + public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) + { + addNode(node); + } + + public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) + { + } + + public void OnNodeStopping() + { + } + + public void OnStorageAvailabilityCreated(StorageAvailability response) + { + } + + public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) + { + } + + public void OnStorageContractUpdated(StoragePurchase purchaseStatus) { - log.Error("Failure during transcript teardown: " + ex); } } } public class CodexDistTest : DistTest { + private static readonly object _lock = new object(); + private static readonly Dictionary writers = new Dictionary(); + private static readonly Dictionary blockCaches = new Dictionary(); + + // this entire structure is not good and needs to be destroyed at the earliest convenience: + private static readonly Dictionary> nodes = new Dictionary>(); + public CodexDistTest() { ProjectPlugin.Load(); @@ -99,12 +112,34 @@ namespace CodexTests localBuilder.Build(); } - protected override void CreateComponents(ILifecycleComponentCollector collector) + protected override void LifecycleStart(TestLifecycle lifecycle) { - base.CreateComponents(collector); - collector.AddComponent(new CodexDistTestComponents( - SetupTranscript() - )); + base.LifecycleStart(lifecycle); + SetupTranscript(lifecycle); + + Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n => + { + lock (_lock) + { + if (!nodes.ContainsKey(lifecycle)) nodes.Add(lifecycle, new List()); + nodes[lifecycle].Add(n); + } + })); + } + + protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) + { + base.LifecycleStop(lifecycle, result); + TeardownTranscript(lifecycle, result); + + if (!result.Success) + { + lock (_lock) + { + var codexNodes = nodes[lifecycle]; + foreach (var node in codexNodes) node.DownloadLog(); + } + } } public ICodexNode StartCodex() @@ -138,11 +173,6 @@ namespace CodexTests return Ci.StartGethNode(GetBlockCache(), setup); } - private BlockCache GetBlockCache() - { - return Get().Cache; - } - public PeerConnectionTestHelpers CreatePeerConnectionTestHelpers() { return new PeerConnectionTestHelpers(GetTestLog()); @@ -150,7 +180,7 @@ namespace CodexTests public PeerDownloadTestHelpers CreatePeerDownloadTestHelpers() { - return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); + return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); } public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "") @@ -228,20 +258,81 @@ namespace CodexTests return null; } - private CodexTranscriptWriter? SetupTranscript() + private void SetupTranscript(TestLifecycle lifecycle) { var attr = GetTranscriptAttributeOfCurrentTest(); - if (attr == null) return null; + if (attr == null) return; var config = new CodexTranscriptWriterConfig( - attr.OutputFilename, attr.IncludeBlockReceivedEvents ); - var log = new LogPrefixer(GetTestLog(), "(Transcript) "); + var log = new LogPrefixer(lifecycle.Log, "(Transcript) "); var writer = new CodexTranscriptWriter(log, config, Transcript.NewWriter(log)); Ci.AddCodexHooksProvider(writer); - return writer; + lock (_lock) + { + writers.Add(lifecycle, writer); + } + } + + private void TeardownTranscript(TestLifecycle lifecycle, DistTestResult result) + { + var attr = GetTranscriptAttributeOfCurrentTest(); + if (attr == null) return; + + var outputFilepath = GetOutputFullPath(lifecycle, attr); + + CodexTranscriptWriter writer = null!; + lock (_lock) + { + writer = writers[lifecycle]; + writers.Remove(lifecycle); + } + + writer.AddResult(result.Success, result.Result); + + try + { + Stopwatch.Measure(lifecycle.Log, "Transcript.ProcessLogs", () => + { + writer.ProcessLogs(lifecycle.DownloadAllLogs()); + }); + + Stopwatch.Measure(lifecycle.Log, $"Transcript.Finalize: {outputFilepath}", () => + { + writer.IncludeFile(lifecycle.Log.GetFullName()); + writer.Finalize(outputFilepath); + }); + } + catch (Exception ex) + { + lifecycle.Log.Error("Failure during transcript teardown: " + ex); + } + } + + private string GetOutputFullPath(TestLifecycle lifecycle, CreateTranscriptAttribute attr) + { + var outputPath = Path.GetDirectoryName(lifecycle.Log.GetFullName()); + if (outputPath == null) throw new Exception("Logfile path is null"); + var filename = Path.GetFileNameWithoutExtension(lifecycle.Log.GetFullName()); + if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); + var outputFile = Path.Combine(outputPath, filename + "_" + attr.OutputFilename); + if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; + return outputFile; + } + + private BlockCache GetBlockCache() + { + var lifecycle = Get(); + lock (_lock) + { + if (!blockCaches.ContainsKey(lifecycle)) + { + blockCaches[lifecycle] = new BlockCache(); + } + } + return blockCaches[lifecycle]; } } diff --git a/Tests/ExperimentalTests/CodexLogTrackerProvider.cs b/Tests/ExperimentalTests/CodexLogTrackerProvider.cs deleted file mode 100644 index 1b2fdd66..00000000 --- a/Tests/ExperimentalTests/CodexLogTrackerProvider.cs +++ /dev/null @@ -1,73 +0,0 @@ -using CodexClient; -using CodexClient.Hooks; -using Utils; - -namespace CodexTests -{ - public class CodexLogTrackerProvider : ICodexHooksProvider - { - private readonly Action addNode; - - public CodexLogTrackerProvider(Action addNode) - { - this.addNode = addNode; - } - - // See TestLifecycle.cs DownloadAllLogs() - public ICodexNodeHooks CreateHooks(string nodeName) - { - return new CodexLogTracker(addNode); - } - - public class CodexLogTracker : ICodexNodeHooks - { - private readonly Action addNode; - - public CodexLogTracker(Action addNode) - { - this.addNode = addNode; - } - - public void OnFileDownloaded(ByteSize size, ContentId cid) - { - } - - public void OnFileDownloading(ContentId cid) - { - } - - public void OnFileUploaded(string uid, ByteSize size, ContentId cid) - { - } - - public void OnFileUploading(string uid, ByteSize size) - { - } - - public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) - { - addNode(node); - } - - public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) - { - } - - public void OnNodeStopping() - { - } - - public void OnStorageAvailabilityCreated(StorageAvailability response) - { - } - - public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) - { - } - - public void OnStorageContractUpdated(StoragePurchase purchaseStatus) - { - } - } - } -} diff --git a/Tests/FrameworkTests/Parallelism.cs b/Tests/FrameworkTests/Parallelism.cs deleted file mode 100644 index 8a877f41..00000000 --- a/Tests/FrameworkTests/Parallelism.cs +++ /dev/null @@ -1,6 +0,0 @@ -using NUnit.Framework; - -[assembly: LevelOfParallelism(100)] -namespace FrameworkTests -{ -} From dd888f30e363562a9db1d2d90e497f3343f73b34 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 25 Apr 2025 15:42:13 +0200 Subject: [PATCH 13/69] excellent cleanup --- .../OverwatchSupport/CodexTranscriptWriter.cs | 15 +- .../CodexTranscriptWriterConfig.cs | 4 +- .../MarketplaceAutoBootstrapDistTest.cs | 20 +- .../NodeTests/BasicInfoTests.cs | 8 +- Tests/CodexReleaseTests/Parallelism.cs | 2 +- Tests/DistTestCore/DistTest.cs | 154 +++++--------- Tests/DistTestCore/Global.cs | 60 ++++++ Tests/DistTestCore/Logs/FixtureLog.cs | 9 +- Tests/DistTestCore/Logs/TestLog.cs | 5 +- Tests/DistTestCore/NameUtils.cs | 14 +- .../AutoBootstrapDistTest.cs | 37 +--- Tests/ExperimentalTests/CodexDistTest.cs | 190 ++++-------------- .../CodexLogTrackerProvider.cs | 73 +++++++ 13 files changed, 258 insertions(+), 333 deletions(-) create mode 100644 Tests/DistTestCore/Global.cs create mode 100644 Tests/ExperimentalTests/CodexLogTrackerProvider.cs diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs index afd8d1a2..95b8dde6 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriter.cs @@ -23,16 +23,27 @@ namespace CodexPlugin.OverwatchSupport converter = new CodexLogConverter(writer, config, identityMap); } - public void Finalize(string outputFilepath) + public void FinalizeWriter() { log.Log("Finalizing Codex transcript..."); writer.AddHeader(CodexHeaderKey, CreateCodexHeader()); - writer.Write(outputFilepath); + writer.Write(GetOutputFullPath()); log.Log("Done"); } + private string GetOutputFullPath() + { + var outputPath = Path.GetDirectoryName(log.GetFullName()); + if (outputPath == null) throw new Exception("Logfile path is null"); + var filename = Path.GetFileNameWithoutExtension(log.GetFullName()); + if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); + var outputFile = Path.Combine(outputPath, filename + "_" + config.OutputPath); + if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; + return outputFile; + } + public ICodexNodeHooks CreateHooks(string nodeName) { nodeName = Str.Between(nodeName, "'", "'"); diff --git a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs index 247494c8..112f5b16 100644 --- a/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs +++ b/ProjectPlugins/CodexPlugin/OverwatchSupport/CodexTranscriptWriterConfig.cs @@ -2,11 +2,13 @@ { public class CodexTranscriptWriterConfig { - public CodexTranscriptWriterConfig(bool includeBlockReceivedEvents) + public CodexTranscriptWriterConfig(string outputPath, bool includeBlockReceivedEvents) { + OutputPath = outputPath; IncludeBlockReceivedEvents = includeBlockReceivedEvents; } + public string OutputPath { get; } public bool IncludeBlockReceivedEvents { get; } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index a5ec0885..96f84813 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -3,41 +3,35 @@ using CodexContractsPlugin; using CodexContractsPlugin.Marketplace; using CodexPlugin; using CodexTests; -using DistTestCore; using GethPlugin; using Nethereum.Hex.HexConvertors.Extensions; +using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests { public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest { - private readonly Dictionary handles = new Dictionary(); + private MarketplaceHandle handle = null!; protected const int StartingBalanceTST = 1000; protected const int StartingBalanceEth = 10; - protected override void LifecycleStart(TestLifecycle lifecycle) + [SetUp] + public void SetupMarketplace() { - base.LifecycleStart(lifecycle); var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); - handles.Add(lifecycle, new MarketplaceHandle(geth, contracts)); - } - - protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) - { - handles.Remove(lifecycle); - base.LifecycleStop(lifecycle, result); + handle = new MarketplaceHandle(geth, contracts); } protected IGethNode GetGeth() { - return handles[Get()].Geth; + return handle.Geth; } protected ICodexContracts GetContracts() { - return handles[Get()].Contracts; + return handle.Contracts; } protected TimeSpan GetPeriodDuration() diff --git a/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs b/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs index 9b9f4bbf..f6cba228 100644 --- a/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs +++ b/Tests/CodexReleaseTests/NodeTests/BasicInfoTests.cs @@ -1,11 +1,5 @@ -using CodexPlugin; -using CodexTests; +using CodexTests; using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Utils; namespace CodexReleaseTests.NodeTests diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index f6e68cc0..d589af32 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(1)] +[assembly: LevelOfParallelism(10)] namespace CodexReleaseTests { } diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 534eb74a..93732405 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -12,70 +12,45 @@ using Assert = NUnit.Framework.Assert; namespace DistTestCore { [Parallelizable(ParallelScope.All)] + [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public abstract class DistTest { - private const string TestNamespacePrefix = "cdx-"; - private readonly Configuration configuration = new Configuration(); - private readonly Assembly[] testAssemblies; + private static readonly Global global = new Global(); private readonly FixtureLog fixtureLog; private readonly StatusLog statusLog; - private readonly object lifecycleLock = new object(); - private readonly EntryPoint globalEntryPoint; - private readonly Dictionary lifecycles = new Dictionary(); - private readonly string deployId; - + private readonly TestLifecycle lifecycle; + private readonly string deployId = NameUtils.MakeDeployId(); + public DistTest() { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray(); - - deployId = NameUtils.MakeDeployId(); - - var logConfig = configuration.GetLogConfig(); + var logConfig = global.Configuration.GetLogConfig(); var startTime = DateTime.UtcNow; fixtureLog = FixtureLog.Create(logConfig, startTime, deployId); statusLog = new StatusLog(logConfig, startTime, "dist-tests", deployId); - globalEntryPoint = new EntryPoint(fixtureLog, configuration.GetK8sConfiguration(new DefaultK8sTimeSet(), TestNamespacePrefix), configuration.GetFileManagerFolder()); + fixtureLog.Log("Test framework revision: " + GitInfo.GetStatus()); + + lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(startTime), global.Configuration, + GetWebCallTimeSet(), + GetK8sTimeSet(), + Global.TestNamespacePrefix + Guid.NewGuid().ToString(), + deployId, + ShouldWaitForCleanup() + ); Initialize(fixtureLog); } [OneTimeSetUp] - public void GlobalSetup() + public static void GlobalSetup() { - fixtureLog.Log($"Starting..."); - globalEntryPoint.Announce(); - - // Previous test run may have been interrupted. - // Begin by cleaning everything up. - try - { - Stopwatch.Measure(fixtureLog, "Global setup", () => - { - globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true); - }); - } - catch (Exception ex) - { - GlobalTestFailure.HasFailed = true; - fixtureLog.Error($"Global setup cleanup failed with: {ex}"); - throw; - } - - fixtureLog.Log("Test framework revision: " + GitInfo.GetStatus()); - fixtureLog.Log("Global setup cleanup successful"); + global.Setup(); } [OneTimeTearDown] - public void GlobalTearDown() + public static void GlobalTearDown() { - globalEntryPoint.Decommission( - // There shouldn't be any of either, but clean everything up regardless. - deleteKubernetesResources: true, - deleteTrackedFiles: true, - waitTillDone: true - ); + global.TearDown(); } [SetUp] @@ -85,18 +60,6 @@ namespace DistTestCore { Assert.Inconclusive("Skip test: Previous test failed during clean up."); } - else - { - try - { - CreateNewTestLifecycle(); - } - catch (Exception ex) - { - fixtureLog.Error("Setup failed: " + ex); - GlobalTestFailure.HasFailed = true; - } - } } [TearDown] @@ -117,18 +80,18 @@ namespace DistTestCore { get { - return Get().CoreInterface; + return lifecycle.CoreInterface; } } public TrackedFile GenerateTestFile(ByteSize size, string label = "") { - return Get().GenerateTestFile(size, label); + return lifecycle.GenerateTestFile(size, label); } public TrackedFile GenerateTestFile(Action options, string label = "") { - return Get().GenerateTestFile(options, label); + return lifecycle.GenerateTestFile(options, label); } /// @@ -137,12 +100,22 @@ namespace DistTestCore /// public void ScopedTestFiles(Action action) { - Get().GetFileManager().ScopedFiles(action); + lifecycle.GetFileManager().ScopedFiles(action); } public ILog GetTestLog() { - return Get().Log; + return lifecycle.Log; + } + + public IFileManager GetFileManager() + { + return lifecycle.GetFileManager(); + } + + public string GetTestNamespace() + { + return lifecycle.TestNamespace; } public void Log(string msg) @@ -159,64 +132,24 @@ namespace DistTestCore public void Measure(string name, Action action) { - Stopwatch.Measure(Get().Log, name, action); + Stopwatch.Measure(lifecycle.Log, name, action); } protected TimeRange GetTestRunTimeRange() { - return new TimeRange(Get().TestStart, DateTime.UtcNow); + return new TimeRange(lifecycle.TestStart, DateTime.UtcNow); } protected virtual void Initialize(FixtureLog fixtureLog) { } - protected virtual void LifecycleStart(TestLifecycle lifecycle) - { - } - - protected virtual void LifecycleStop(TestLifecycle lifecycle, DistTestResult testResult) - { - } - protected virtual void CollectStatusLogData(TestLifecycle lifecycle, Dictionary data) { } - protected TestLifecycle Get() - { - lock (lifecycleLock) - { - return lifecycles[GetCurrentTestName()]; - } - } - - private void CreateNewTestLifecycle() - { - var testName = GetCurrentTestName(); - fixtureLog.WriteLogTag(); - Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () => - { - lock (lifecycleLock) - { - var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); - var lifecycle = new TestLifecycle( - fixtureLog.CreateTestLog(), - configuration, - GetWebCallTimeSet(), - GetK8sTimeSet(), - testNamespace, - deployId, - ShouldWaitForCleanup()); - lifecycles.Add(testName, lifecycle); - LifecycleStart(lifecycle); - } - }); - } - private void DisposeTestLifecycle() { - var lifecycle = Get(); var testResult = GetTestResult(); var testDuration = lifecycle.GetTestDuration(); var data = lifecycle.GetPluginMetadata(); @@ -228,9 +161,7 @@ namespace DistTestCore WriteEndTestLog(lifecycle.Log); IncludeLogsOnTestFailure(lifecycle); - LifecycleStop(lifecycle, testResult); lifecycle.DeleteAllResources(); - lifecycles.Remove(GetCurrentTestName()); }); } @@ -287,7 +218,7 @@ namespace DistTestCore var className = currentTest.ClassName; var methodName = currentTest.MethodName; - var testClasses = testAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray(); + var testClasses = global.TestAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray(); var testMethods = testClasses.SelectMany(c => c.GetMethods()).Where(m => m.Name == methodName).ToArray(); return testMethods.Select(m => m.GetCustomAttribute()) @@ -296,19 +227,24 @@ namespace DistTestCore .ToArray(); } + protected IDownloadedLog[] DownloadAllLogs() + { + return lifecycle.DownloadAllLogs(); + } + private void IncludeLogsOnTestFailure(TestLifecycle lifecycle) { var testStatus = TestContext.CurrentContext.Result.Outcome.Status; if (ShouldDownloadAllLogs(testStatus)) { lifecycle.Log.Log("Downloading all container logs..."); - lifecycle.DownloadAllLogs(); + DownloadAllLogs(); } } private bool ShouldDownloadAllLogs(TestStatus testStatus) { - if (configuration.AlwaysDownloadContainerLogs) return true; + if (global.Configuration.AlwaysDownloadContainerLogs) return true; if (!IsDownloadingLogsEnabled()) return false; if (testStatus == TestStatus.Failed) { @@ -323,7 +259,7 @@ namespace DistTestCore return $"[{TestContext.CurrentContext.Test.Name}]"; } - private DistTestResult GetTestResult() + public DistTestResult GetTestResult() { var success = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed; var status = TestContext.CurrentContext.Result.Outcome.Status.ToString(); diff --git a/Tests/DistTestCore/Global.cs b/Tests/DistTestCore/Global.cs new file mode 100644 index 00000000..4ba1374b --- /dev/null +++ b/Tests/DistTestCore/Global.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using Core; +using Logging; + +namespace DistTestCore +{ + public class Global + { + public const string TestNamespacePrefix = "cdx-"; + public Configuration Configuration { get; } = new Configuration(); + + public Assembly[] TestAssemblies { get; } + private readonly EntryPoint globalEntryPoint; + private readonly ILog log; + + public Global() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + TestAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray(); + + log = new ConsoleLog(); + globalEntryPoint = new EntryPoint( + log, + Configuration.GetK8sConfiguration( + new DefaultK8sTimeSet(), + TestNamespacePrefix + ), + Configuration.GetFileManagerFolder() + ); + } + + public void Setup() + { + try + { + Stopwatch.Measure(log, "Global setup", () => + { + globalEntryPoint.Announce(); + globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true); + }); + } + catch (Exception ex) + { + GlobalTestFailure.HasFailed = true; + log.Error($"Global setup cleanup failed with: {ex}"); + throw; + } + } + + public void TearDown() + { + globalEntryPoint.Decommission( + // There shouldn't be any of either, but clean everything up regardless. + deleteKubernetesResources: true, + deleteTrackedFiles: true, + waitTillDone: true + ); + } + } +} diff --git a/Tests/DistTestCore/Logs/FixtureLog.cs b/Tests/DistTestCore/Logs/FixtureLog.cs index 9d3c77d3..5978a602 100644 --- a/Tests/DistTestCore/Logs/FixtureLog.cs +++ b/Tests/DistTestCore/Logs/FixtureLog.cs @@ -4,19 +4,14 @@ namespace DistTestCore.Logs { public class FixtureLog : BaseTestLog { - private readonly ILog backingLog; - private readonly string deployId; - public FixtureLog(ILog backingLog, string deployId) : base(backingLog, deployId) { - this.backingLog = backingLog; - this.deployId = deployId; } - public TestLog CreateTestLog(string name = "") + public TestLog CreateTestLog(DateTime start, string name = "") { - return TestLog.Create(this, name); + return TestLog.Create(this, start, name); } public static FixtureLog Create(LogConfig config, DateTime start, string deployId, string name = "") diff --git a/Tests/DistTestCore/Logs/TestLog.cs b/Tests/DistTestCore/Logs/TestLog.cs index 0dca1464..5c38f951 100644 --- a/Tests/DistTestCore/Logs/TestLog.cs +++ b/Tests/DistTestCore/Logs/TestLog.cs @@ -1,5 +1,4 @@ using Logging; -using System.Xml.Linq; namespace DistTestCore.Logs { @@ -11,9 +10,9 @@ namespace DistTestCore.Logs backingLog.Log($"*** Begin: {methodName}"); } - public static TestLog Create(FixtureLog parentLog, string name = "") + public static TestLog Create(FixtureLog parentLog, DateTime start, string name = "") { - var methodName = NameUtils.GetTestMethodName(name); + var methodName = NameUtils.GetTestLogFileName(start, name); var fullName = Path.Combine(parentLog.GetFullName(), methodName); var backingLog = CreateMainLog(fullName, name); return new TestLog(backingLog, methodName, parentLog.DeployId); diff --git a/Tests/DistTestCore/NameUtils.cs b/Tests/DistTestCore/NameUtils.cs index 55489db9..44919d51 100644 --- a/Tests/DistTestCore/NameUtils.cs +++ b/Tests/DistTestCore/NameUtils.cs @@ -5,6 +5,11 @@ namespace DistTestCore { public static class NameUtils { + public static string GetTestLogFileName(DateTime start, string name = "") + { + return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{GetTestMethodName(name)}"; + } + public static string GetTestMethodName(string name = "") { if (!string.IsNullOrEmpty(name)) return name; @@ -16,7 +21,7 @@ namespace DistTestCore public static string GetFixtureFullName(LogConfig config, DateTime start, string name) { var folder = DetermineFolder(config, start); - var fixtureName = GetFixtureName(name, start); + var fixtureName = GetRawFixtureName(); return Path.Combine(folder, fixtureName); } @@ -85,13 +90,6 @@ namespace DistTestCore Pad(start.Day)); } - private static string GetFixtureName(string name, DateTime start) - { - var fixtureName = GetRawFixtureName(); - if (!string.IsNullOrEmpty(name)) fixtureName = name; - return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{fixtureName.Replace('.', '-')}"; - } - private static string Pad(int n) { return n.ToString().PadLeft(2, '0'); diff --git a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs index d8ac6805..93fcf6f7 100644 --- a/Tests/ExperimentalTests/AutoBootstrapDistTest.cs +++ b/Tests/ExperimentalTests/AutoBootstrapDistTest.cs @@ -1,29 +1,27 @@ using CodexClient; using CodexPlugin; -using DistTestCore; +using NUnit.Framework; namespace CodexTests { public class AutoBootstrapDistTest : CodexDistTest { - private readonly Dictionary bootstrapNodes = new Dictionary(); private bool isBooting = false; - protected override void LifecycleStart(TestLifecycle tl) + public ICodexNode BootstrapNode { get; private set; } = null!; + + [SetUp] + public void SetupBootstrapNode() { - base.LifecycleStart(tl); - if (!bootstrapNodes.ContainsKey(tl)) - { - isBooting = true; - bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace))); - isBooting = false; - } + isBooting = true; + BootstrapNode = StartCodex(s => s.WithName("BOOTSTRAP_" + GetTestNamespace())); + isBooting = false; } - protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) + [TearDown] + public void TearDownBootstrapNode() { - bootstrapNodes.Remove(lifecycle); - base.LifecycleStop(lifecycle, result); + BootstrapNode.Stop(waitTillStopped: false); } protected override void OnCodexSetup(ICodexSetup setup) @@ -33,18 +31,5 @@ namespace CodexTests var node = BootstrapNode; if (node != null) setup.WithBootstrapNode(node); } - - protected ICodexNode BootstrapNode - { - get - { - var tl = Get(); - if (bootstrapNodes.TryGetValue(tl, out var node)) - { - return node; - } - throw new InvalidOperationException("Bootstrap node not yet started."); - } - } } } diff --git a/Tests/ExperimentalTests/CodexDistTest.cs b/Tests/ExperimentalTests/CodexDistTest.cs index d27216d3..5ecae330 100644 --- a/Tests/ExperimentalTests/CodexDistTest.cs +++ b/Tests/ExperimentalTests/CodexDistTest.cs @@ -1,6 +1,5 @@ using BlockchainUtils; using CodexClient; -using CodexClient.Hooks; using CodexContractsPlugin; using CodexNetDeployer; using CodexPlugin; @@ -17,85 +16,14 @@ using Newtonsoft.Json; using NUnit.Framework; using NUnit.Framework.Constraints; using OverwatchTranscript; -using Utils; namespace CodexTests { - public class CodexLogTrackerProvider : ICodexHooksProvider - { - private readonly Action addNode; - - public CodexLogTrackerProvider(Action addNode) - { - this.addNode = addNode; - } - - // See TestLifecycle.cs DownloadAllLogs() - public ICodexNodeHooks CreateHooks(string nodeName) - { - return new CodexLogTracker(addNode); - } - - public class CodexLogTracker : ICodexNodeHooks - { - private readonly Action addNode; - - public CodexLogTracker(Action addNode) - { - this.addNode = addNode; - } - - public void OnFileDownloaded(ByteSize size, ContentId cid) - { - } - - public void OnFileDownloading(ContentId cid) - { - } - - public void OnFileUploaded(string uid, ByteSize size, ContentId cid) - { - } - - public void OnFileUploading(string uid, ByteSize size) - { - } - - public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) - { - addNode(node); - } - - public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) - { - } - - public void OnNodeStopping() - { - } - - public void OnStorageAvailabilityCreated(StorageAvailability response) - { - } - - public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) - { - } - - public void OnStorageContractUpdated(StoragePurchase purchaseStatus) - { - } - } - } - public class CodexDistTest : DistTest { - private static readonly object _lock = new object(); - private static readonly Dictionary writers = new Dictionary(); - private static readonly Dictionary blockCaches = new Dictionary(); - - // this entire structure is not good and needs to be destroyed at the earliest convenience: - private static readonly Dictionary> nodes = new Dictionary>(); + private readonly BlockCache blockCache = new BlockCache(); + private readonly List nodes = new List(); + private CodexTranscriptWriter? writer; public CodexDistTest() { @@ -105,41 +33,26 @@ namespace CodexTests ProjectPlugin.Load(); } + [SetUp] + public void SetupCodexDistTest() + { + writer = SetupTranscript(); + + } + + [TearDown] + public void TearDownCodexDistTest() + { + TeardownTranscript(); + } + protected override void Initialize(FixtureLog fixtureLog) { var localBuilder = new LocalCodexBuilder(fixtureLog); localBuilder.Intialize(); localBuilder.Build(); - } - protected override void LifecycleStart(TestLifecycle lifecycle) - { - base.LifecycleStart(lifecycle); - SetupTranscript(lifecycle); - - Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n => - { - lock (_lock) - { - if (!nodes.ContainsKey(lifecycle)) nodes.Add(lifecycle, new List()); - nodes[lifecycle].Add(n); - } - })); - } - - protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result) - { - base.LifecycleStop(lifecycle, result); - TeardownTranscript(lifecycle, result); - - if (!result.Success) - { - lock (_lock) - { - var codexNodes = nodes[lifecycle]; - foreach (var node in codexNodes) node.DownloadLog(); - } - } + Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(nodes.Add)); } public ICodexNode StartCodex() @@ -170,7 +83,7 @@ namespace CodexTests public IGethNode StartGethNode(Action setup) { - return Ci.StartGethNode(GetBlockCache(), setup); + return Ci.StartGethNode(blockCache, setup); } public PeerConnectionTestHelpers CreatePeerConnectionTestHelpers() @@ -180,7 +93,7 @@ namespace CodexTests public PeerDownloadTestHelpers CreatePeerDownloadTestHelpers() { - return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); + return new PeerDownloadTestHelpers(GetTestLog(), GetFileManager()); } public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "") @@ -258,82 +171,47 @@ namespace CodexTests return null; } - private void SetupTranscript(TestLifecycle lifecycle) + private CodexTranscriptWriter? SetupTranscript() { var attr = GetTranscriptAttributeOfCurrentTest(); - if (attr == null) return; + if (attr == null) return null; var config = new CodexTranscriptWriterConfig( + attr.OutputFilename, attr.IncludeBlockReceivedEvents ); - var log = new LogPrefixer(lifecycle.Log, "(Transcript) "); + var log = new LogPrefixer(GetTestLog(), "(Transcript) "); var writer = new CodexTranscriptWriter(log, config, Transcript.NewWriter(log)); Ci.AddCodexHooksProvider(writer); - lock (_lock) - { - writers.Add(lifecycle, writer); - } + return writer; } - private void TeardownTranscript(TestLifecycle lifecycle, DistTestResult result) + private void TeardownTranscript() { - var attr = GetTranscriptAttributeOfCurrentTest(); - if (attr == null) return; - - var outputFilepath = GetOutputFullPath(lifecycle, attr); - - CodexTranscriptWriter writer = null!; - lock (_lock) - { - writer = writers[lifecycle]; - writers.Remove(lifecycle); - } + if (writer == null) return; + var result = GetTestResult(); + var log = GetTestLog(); writer.AddResult(result.Success, result.Result); - try { - Stopwatch.Measure(lifecycle.Log, "Transcript.ProcessLogs", () => + Stopwatch.Measure(log, "Transcript.ProcessLogs", () => { - writer.ProcessLogs(lifecycle.DownloadAllLogs()); + writer.ProcessLogs(DownloadAllLogs()); }); - Stopwatch.Measure(lifecycle.Log, $"Transcript.Finalize: {outputFilepath}", () => + Stopwatch.Measure(log, $"Transcript.FinalizeWriter", () => { - writer.IncludeFile(lifecycle.Log.GetFullName()); - writer.Finalize(outputFilepath); + writer.IncludeFile(log.GetFullName()); + writer.FinalizeWriter(); }); } catch (Exception ex) { - lifecycle.Log.Error("Failure during transcript teardown: " + ex); + log.Error("Failure during transcript teardown: " + ex); } } - - private string GetOutputFullPath(TestLifecycle lifecycle, CreateTranscriptAttribute attr) - { - var outputPath = Path.GetDirectoryName(lifecycle.Log.GetFullName()); - if (outputPath == null) throw new Exception("Logfile path is null"); - var filename = Path.GetFileNameWithoutExtension(lifecycle.Log.GetFullName()); - if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty"); - var outputFile = Path.Combine(outputPath, filename + "_" + attr.OutputFilename); - if (!outputFile.EndsWith(".owts")) outputFile += ".owts"; - return outputFile; - } - - private BlockCache GetBlockCache() - { - var lifecycle = Get(); - lock (_lock) - { - if (!blockCaches.ContainsKey(lifecycle)) - { - blockCaches[lifecycle] = new BlockCache(); - } - } - return blockCaches[lifecycle]; - } } [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] diff --git a/Tests/ExperimentalTests/CodexLogTrackerProvider.cs b/Tests/ExperimentalTests/CodexLogTrackerProvider.cs new file mode 100644 index 00000000..1b2fdd66 --- /dev/null +++ b/Tests/ExperimentalTests/CodexLogTrackerProvider.cs @@ -0,0 +1,73 @@ +using CodexClient; +using CodexClient.Hooks; +using Utils; + +namespace CodexTests +{ + public class CodexLogTrackerProvider : ICodexHooksProvider + { + private readonly Action addNode; + + public CodexLogTrackerProvider(Action addNode) + { + this.addNode = addNode; + } + + // See TestLifecycle.cs DownloadAllLogs() + public ICodexNodeHooks CreateHooks(string nodeName) + { + return new CodexLogTracker(addNode); + } + + public class CodexLogTracker : ICodexNodeHooks + { + private readonly Action addNode; + + public CodexLogTracker(Action addNode) + { + this.addNode = addNode; + } + + public void OnFileDownloaded(ByteSize size, ContentId cid) + { + } + + public void OnFileDownloading(ContentId cid) + { + } + + public void OnFileUploaded(string uid, ByteSize size, ContentId cid) + { + } + + public void OnFileUploading(string uid, ByteSize size) + { + } + + public void OnNodeStarted(ICodexNode node, string peerId, string nodeId) + { + addNode(node); + } + + public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount) + { + } + + public void OnNodeStopping() + { + } + + public void OnStorageAvailabilityCreated(StorageAvailability response) + { + } + + public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract) + { + } + + public void OnStorageContractUpdated(StoragePurchase purchaseStatus) + { + } + } + } +} From 24a25292b8f12ef2788167a628499c959a3906e5 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 25 Apr 2025 16:13:01 +0200 Subject: [PATCH 14/69] fixes transcript writing --- Framework/Utils/Str.cs | 5 ++++- Tests/ExperimentalTests/CodexDistTest.cs | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Framework/Utils/Str.cs b/Framework/Utils/Str.cs index aea19ec8..ab8fe46d 100644 --- a/Framework/Utils/Str.cs +++ b/Framework/Utils/Str.cs @@ -4,8 +4,11 @@ { public static string Between(string input, string open, string close) { - var openIndex = input.IndexOf(open) + open.Length; + var openI = input.IndexOf(open); + if (openI == -1) return input; + var openIndex = openI + open.Length; var closeIndex = input.LastIndexOf(close); + if (closeIndex == -1) return input; return input.Substring(openIndex, closeIndex - openIndex); } diff --git a/Tests/ExperimentalTests/CodexDistTest.cs b/Tests/ExperimentalTests/CodexDistTest.cs index 5ecae330..422b5bd7 100644 --- a/Tests/ExperimentalTests/CodexDistTest.cs +++ b/Tests/ExperimentalTests/CodexDistTest.cs @@ -37,7 +37,6 @@ namespace CodexTests public void SetupCodexDistTest() { writer = SetupTranscript(); - } [TearDown] @@ -203,7 +202,7 @@ namespace CodexTests Stopwatch.Measure(log, $"Transcript.FinalizeWriter", () => { - writer.IncludeFile(log.GetFullName()); + writer.IncludeFile(log.GetFullName() + ".log"); writer.FinalizeWriter(); }); } From ff78d7e28e877a89965cedcc63586ed2d650926b Mon Sep 17 00:00:00 2001 From: ThatBen Date: Sun, 27 Apr 2025 12:10:45 +0200 Subject: [PATCH 15/69] fixes crash in chain monitor --- .../MarketTests/ChainMonitor.cs | 19 ++++++++++++++----- .../MarketplaceAutoBootstrapDistTest.cs | 6 +++++- .../MarketTests/MultipleContractsTest.cs | 9 +++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs index 74f6a96a..f9e6b9d6 100644 --- a/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs +++ b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs @@ -26,10 +26,10 @@ namespace CodexReleaseTests.MarketTests this.updateInterval = updateInterval; } - public void Start() + public void Start(Action onFailure) { cts = new CancellationTokenSource(); - worker = Task.Run(Worker); + worker = Task.Run(() => Worker(onFailure)); } public void Stop() @@ -39,14 +39,23 @@ namespace CodexReleaseTests.MarketTests if (worker.Exception != null) throw worker.Exception; } - private void Worker() + private void Worker(Action onFailure) { var state = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, doProofPeriodMonitoring: true); Thread.Sleep(updateInterval); while (!cts.IsCancellationRequested) { - UpdateChainState(state); + try + { + UpdateChainState(state); + } + catch (Exception ex) + { + log.Error("Exception in chain monitor: " + ex); + onFailure(); + throw; + } cts.Token.WaitHandle.WaitOne(updateInterval); } @@ -61,7 +70,7 @@ namespace CodexReleaseTests.MarketTests var slots = reports.Reports.Sum(r => Convert.ToInt32(r.TotalNumSlots)); var required = reports.Reports.Sum(r => Convert.ToInt32(r.TotalProofsRequired)); - var missed = reports.Reports.Sum(r => Convert.ToInt32(r.MissedProofs)); + var missed = reports.Reports.Sum(r => r.MissedProofs.Length); log.Log($"Proof report: Slots={slots} Required={required} Missed={missed}"); } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index b0db0702..49a3bcd5 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -7,6 +7,7 @@ using DistTestCore; using GethPlugin; using Logging; using Nethereum.Hex.HexConvertors.Extensions; +using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests @@ -123,7 +124,10 @@ namespace CodexReleaseTests.MarketTests if (!MonitorChainState) return null; var result = new ChainMonitor(log, contracts, startUtc); - result.Start(); + result.Start(() => + { + Assert.Fail("Failure in chain monitor."); + }); return result; } diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 69fe413b..d6173960 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -43,20 +43,19 @@ namespace CodexReleaseTests.MarketTests [Values(5, 10)] int numGenerations) { var hosts = StartHosts(); + var clients = StartClients(); for (var i = 0; i < numGenerations; i++) { Log("Generation: " + i); - Generation(hosts); + Generation(clients, hosts); } Thread.Sleep(TimeSpan.FromSeconds(12.0)); } - private void Generation(ICodexNodeGroup hosts) + private void Generation(ICodexNodeGroup clients, ICodexNodeGroup hosts) { - var clients = StartClients(); - var requests = clients.Select(CreateStorageRequest).ToArray(); All(requests, r => @@ -67,8 +66,6 @@ namespace CodexReleaseTests.MarketTests All(requests, r => r.WaitForStorageContractStarted()); - clients.Stop(waitTillStopped: false); - // for the time being, we're only interested in whether these contracts start. //All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); //All(requests, r => r.WaitForStorageContractFinished()); From 54227685ecc5c37348f70f99bdf48caa28240518 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 28 Apr 2025 12:20:36 +0200 Subject: [PATCH 16/69] debugging contract start --- .../CodexPlugin/CodexDockerImage.cs | 2 +- .../MarketTests/MultipleContractsTest.cs | 21 ++++++++++++++----- Tests/CodexReleaseTests/Parallelism.cs | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index ec8d6395..afcfc747 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,7 +2,7 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-97e9684-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-1932aec-dist-tests"; //"codexstorage/nim-codex:0.2.1-dist-tests"; public static string Override { get; set; } = string.Empty; diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index d6173960..e217b280 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -32,8 +32,8 @@ namespace CodexReleaseTests.MarketTests private readonly int tolerance; protected override int NumberOfHosts => hosts; - protected override int NumberOfClients => 3; - protected override ByteSize HostAvailabilitySize => (100 * FilesizeMb).MB(); + protected override int NumberOfClients => 6; + protected override ByteSize HostAvailabilitySize => (1000 * FilesizeMb).MB(); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); @@ -56,7 +56,7 @@ namespace CodexReleaseTests.MarketTests private void Generation(ICodexNodeGroup clients, ICodexNodeGroup hosts) { - var requests = clients.Select(CreateStorageRequest).ToArray(); + var requests = All(clients.ToArray(), CreateStorageRequest); All(requests, r => { @@ -71,9 +71,9 @@ namespace CodexReleaseTests.MarketTests //All(requests, r => r.WaitForStorageContractFinished()); } - private void All(IStoragePurchaseContract[] requests, Action action) + private void All(T[] items, Action action) { - var tasks = requests.Select(r => Task.Run(() => action(r))).ToArray(); + var tasks = items.Select(r => Task.Run(() => action(r))).ToArray(); Task.WaitAll(tasks); foreach(var t in tasks) { @@ -81,6 +81,17 @@ namespace CodexReleaseTests.MarketTests } } + private TResult[] All(T[] items, Func action) + { + var tasks = items.Select(r => Task.Run(() => action(r))).ToArray(); + Task.WaitAll(tasks); + foreach (var t in tasks) + { + if (t.Exception != null) throw t.Exception; + } + return tasks.Select(t => t.Result).ToArray(); + } + private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { var cid = client.UploadFile(GenerateTestFile(FilesizeMb.MB())); diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index ab6a11fb..a1b26c73 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(3)] +[assembly: LevelOfParallelism(1)] namespace CodexReleaseTests.DataTests { } From 369996afd5bf2503cb7b72ad18d67f5f3bbac87c Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 28 Apr 2025 15:29:40 +0200 Subject: [PATCH 17/69] strip some tests --- .../MarketTests/ContractSuccessfulTest.cs | 6 ------ .../MarketTests/MultipleContractsTest.cs | 9 +-------- Tests/CodexReleaseTests/Parallelism.cs | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 9aa22eab..9c1ab2e0 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -7,14 +7,8 @@ namespace CodexReleaseTests.MarketTests [TestFixture(6, 3, 1)] [TestFixture(6, 4, 2)] [TestFixture(8, 5, 1)] - [TestFixture(8, 5, 2)] [TestFixture(8, 6, 1)] - [TestFixture(8, 6, 2)] [TestFixture(8, 6, 3)] - [TestFixture(8, 8, 1)] - [TestFixture(8, 8, 2)] - [TestFixture(8, 8, 3)] - [TestFixture(8, 8, 4)] public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest { public ContractSuccessfulTest(int hosts, int slots, int tolerance) diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index e217b280..672f9eee 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -7,16 +7,9 @@ namespace CodexReleaseTests.MarketTests { [TestFixture(6, 3, 1)] [TestFixture(6, 4, 1)] - [TestFixture(6, 4, 2)] [TestFixture(8, 5, 1)] - [TestFixture(8, 5, 2)] [TestFixture(8, 6, 1)] - [TestFixture(8, 6, 2)] [TestFixture(8, 6, 3)] - [TestFixture(8, 8, 1)] - [TestFixture(8, 8, 2)] - [TestFixture(8, 8, 3)] - [TestFixture(8, 8, 4)] public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest { public MultipleContractsTest(int hosts, int slots, int tolerance) @@ -40,7 +33,7 @@ namespace CodexReleaseTests.MarketTests [Test] [Combinatorial] public void MultipleContractGenerations( - [Values(5, 10)] int numGenerations) + [Values(10)] int numGenerations) { var hosts = StartHosts(); var clients = StartClients(); diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index a1b26c73..eb2da742 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(1)] +[assembly: LevelOfParallelism(4)] namespace CodexReleaseTests.DataTests { } From 8d50c008b8dbefee74f238e5ea9cae126393b681 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 28 Apr 2025 15:44:46 +0200 Subject: [PATCH 18/69] disables pod ips for cluster testing, fix when not at offsite --- Framework/KubernetesWorkflow/K8sController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index ffcac5df..8684dc86 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -747,7 +747,7 @@ namespace KubernetesWorkflow } var pod = pods[0]; if (pod.Status == null) throw new Exception("Pod status unknown"); - if (string.IsNullOrEmpty(pod.Status.PodIP)) throw new Exception("Pod IP unknown"); + //if (string.IsNullOrEmpty(pod.Status.PodIP)) throw new Exception("Pod IP unknown"); return pod; } @@ -977,7 +977,7 @@ namespace KubernetesWorkflow private PodInfo CreatePodInfo(V1Pod pod) { var name = pod.Name(); - var ip = pod.Status.PodIP; + var ip = "disabled"; // pod.Status.PodIP; var k8sNodeName = pod.Spec.NodeName; if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure."); From 76ea98e783fd2b89eba6aab513fc45f9d04631f1 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 29 Apr 2025 12:03:13 +0200 Subject: [PATCH 19/69] reverse disable pod ip --- Framework/KubernetesWorkflow/K8sController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index 8684dc86..ffcac5df 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -747,7 +747,7 @@ namespace KubernetesWorkflow } var pod = pods[0]; if (pod.Status == null) throw new Exception("Pod status unknown"); - //if (string.IsNullOrEmpty(pod.Status.PodIP)) throw new Exception("Pod IP unknown"); + if (string.IsNullOrEmpty(pod.Status.PodIP)) throw new Exception("Pod IP unknown"); return pod; } @@ -977,7 +977,7 @@ namespace KubernetesWorkflow private PodInfo CreatePodInfo(V1Pod pod) { var name = pod.Name(); - var ip = "disabled"; // pod.Status.PodIP; + var ip = pod.Status.PodIP; var k8sNodeName = pod.Spec.NodeName; if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure."); From acf5436d388448886d201fab4d296a1329fe72cd Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 29 Apr 2025 12:05:33 +0200 Subject: [PATCH 20/69] remove lifecycle test test --- Tests/FrameworkTests/LifecycelyTest.cs | 219 ------------------------- 1 file changed, 219 deletions(-) delete mode 100644 Tests/FrameworkTests/LifecycelyTest.cs diff --git a/Tests/FrameworkTests/LifecycelyTest.cs b/Tests/FrameworkTests/LifecycelyTest.cs deleted file mode 100644 index e420a911..00000000 --- a/Tests/FrameworkTests/LifecycelyTest.cs +++ /dev/null @@ -1,219 +0,0 @@ -using NUnit.Framework; - -namespace FrameworkTests -{ - [Parallelizable(ParallelScope.All)] - [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] - [TestFixture(10)] - [TestFixture(20)] - [TestFixture(30)] - public class LifecycelyTest - { - public LifecycelyTest(int num) - { - Log("ctor", GetCurrentTestName(), num); - this.num = num; - } - - [SetUp] - public void Setup() - { - Log(nameof(Setup), GetCurrentTestName()); - } - - [TearDown] - public void TearDown() - { - Log(nameof(TearDown), GetCurrentTestName()); - } - - //[Test] - //public void A() - //{ - // Log(nameof(A), "Run"); - // SleepRandom(); - // Log(nameof(A), "Finish"); - //} - - //[Test] - //public void B() - //{ - // Log(nameof(B), "Run"); - // SleepRandom(); - // Log(nameof(B), "Finish"); - //} - - //[Test] - //public void C() - //{ - // Log(nameof(C), "Run"); - // SleepRandom(); - // Log(nameof(C), "Finish"); - //} - - [Test] - [Combinatorial] - public void Multi( - [Values(1, 2, 3)] int num) - { - Log(nameof(Multi), "Run", num); - SleepRandom(); - Log(nameof(Multi), "Finish", num); - } - - - - - - - - - - - - - private static readonly Random r = new Random(); - private readonly int num; - - private void SleepRandom() - { - Thread.Sleep(TimeSpan.FromSeconds(5.0)); - Thread.Sleep(TimeSpan.FromMilliseconds(r.Next(100, 1000))); - } - - private void Log(string scope, string msg) - { - ALog.Log($"{num} {scope} {msg}"); - } - - private void Log(string scope, string msg, int num) - { - ALog.Log($"{this.num} {scope} {msg} {num}"); - } - - private string GetCurrentTestName() - { - return $"[{TestContext.CurrentContext.Test.Name}]"; - } - } - - - - - public class ALog - { - private static readonly object _lock = new object(); - - public static void Log(string msg) - { - lock (_lock) - { - File.AppendAllLines("C:\\Users\\vexor\\Desktop\\Alog.txt", [msg]); - } - } - } - - - - - - - public interface ITestLifecycleComponent - { - } - - - - - - public class Base - { - private readonly Dictionary> anyFields = new(); - - public void Setup() - { - var testId = 23; - - var fields = new Dictionary(); - anyFields.Add(testId, fields); - YieldFields(field => - { - fields.Add(field.GetType(), field); - }); - - } - - public void TearDown() - { - var testId = 23; - - // foreach stop - - anyFields.Remove(testId); - } - - public T Get() - { - int testId = 123; - var fields = anyFields[testId]; - var type = typeof(T); - var result = fields[type]; - return (T)result; - } - - public BaseFields GetBaseField() - { - return Get(); - } - - protected virtual void YieldFields(Action giveField) - { - giveField(new BaseFields()); - } - } - - public class Mid : Base - { - protected override void YieldFields(Action giveField) - { - base.YieldFields(giveField); - giveField(new MidFields()); - } - - public MidFields GetMid() - { - return Get(); - } - } - - public class Top : Mid - { - protected override void YieldFields(Action giveField) - { - base.YieldFields(giveField); - giveField(new TopFields()); - } - - public TopFields GetTop() - { - return Get(); - } - } - - public class BaseFields : ITestLifecycleComponent - { - public string EntryPoint { get; set; } = string.Empty; - public string Log { get; set; } = string.Empty; - } - - public class MidFields : ITestLifecycleComponent - { - public string Nodes { get; set; } = string.Empty; - } - - public class TopFields : ITestLifecycleComponent - { - public string Geth { get; set; } = string.Empty; - public string Contracts { get; set; } = string.Empty; - } -} From 90bf718e582c945393aa913edeb7b337dc472dce Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 29 Apr 2025 12:14:06 +0200 Subject: [PATCH 21/69] mergey mistake --- .../MarketTests/MarketplaceAutoBootstrapDistTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index f7d94473..72a56681 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -23,7 +23,7 @@ namespace CodexReleaseTests.MarketTests var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); var monitor = SetupChainMonitor(GetTestLog(), contracts, GetTestRunTimeRange().From); - handle = new MarketplaceHandle(geth, contracts, monitor)); + handle = new MarketplaceHandle(geth, contracts, monitor); } [TearDown] From 2971e67a512071440ab77320669791f50adbbde4 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 29 Apr 2025 14:26:33 +0200 Subject: [PATCH 22/69] wip fix for race condition in blocktimefinder --- Framework/BlockchainUtils/BlockTimeFinder.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Framework/BlockchainUtils/BlockTimeFinder.cs b/Framework/BlockchainUtils/BlockTimeFinder.cs index a84cda3a..c6f10573 100644 --- a/Framework/BlockchainUtils/BlockTimeFinder.cs +++ b/Framework/BlockchainUtils/BlockTimeFinder.cs @@ -101,10 +101,18 @@ namespace BlockchainUtils previous.Utc < target; } - private BlockTimeEntry GetBlock(ulong number) + private BlockTimeEntry GetBlock(ulong number, bool retry = false) { if (number < bounds.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis."); - if (number > bounds.Current.BlockNumber) throw new Exception("Can't fetch block after current."); + if (number > bounds.Current.BlockNumber) + { + if (retry) throw new Exception("Can't fetch block after current."); + + // todo test and verify this: + Thread.Sleep(1000); + bounds.Initialize(); + return GetBlock(number, retry: true); + } var dateTime = web3.GetTimestampForBlock(number); if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist."); From 17ffb41e150b5b133697cc99707b80626ea2a562 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 2 May 2025 08:01:44 +0200 Subject: [PATCH 23/69] Makes proof period report interval configurable default to 24 hours --- Tools/TestNetRewarder/Configuration.cs | 3 +++ Tools/TestNetRewarder/Processor.cs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tools/TestNetRewarder/Configuration.cs b/Tools/TestNetRewarder/Configuration.cs index 5dbeac30..b3aa84cb 100644 --- a/Tools/TestNetRewarder/Configuration.cs +++ b/Tools/TestNetRewarder/Configuration.cs @@ -33,6 +33,9 @@ namespace TestNetRewarder [Uniform("proof-submitted-events", "pse", "PROOFSUBMITTEDEVENTS", false, "When greater than zero, chain event summary will include proof-submitted events.")] public int ShowProofSubmittedEvents { get; set; } = 0; // Defaulted to zero, aprox 7 to 10 such events every 2 minutes in testnet (from autoclient alone!) + [Uniform("proof-period-report-hours", "pprh", "PROOFPERIODREPORTHOURS", false, "Frequency in hours with which proof period reports are created.")] + public int ProofReportHours { get; set; } = 24; + public string LogPath { get diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index eed26962..649ebf47 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -22,6 +22,8 @@ namespace TestNetRewarder this.log = log; lastPeriodUpdateUtc = DateTime.UtcNow; + if (config.ProofReportHours < 1) throw new Exception("ProofReportHours must be one or greater"); + builder = new RequestBuilder(); eventsFormatter = new EventsFormatter(config); @@ -79,7 +81,7 @@ namespace TestNetRewarder private void ProcessPeriodUpdate() { if (config.ShowProofPeriodReports < 1) return; - if (DateTime.UtcNow < (lastPeriodUpdateUtc + TimeSpan.FromHours(1.0))) return; + if (DateTime.UtcNow < (lastPeriodUpdateUtc + TimeSpan.FromHours(config.ProofReportHours))) return; lastPeriodUpdateUtc = DateTime.UtcNow; eventsFormatter.ProcessPeriodReports(chainState.PeriodMonitor.GetAndClearReports()); From c68d4bb13f073c4c632f2f83ebf2a389934eb497 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 2 May 2025 08:21:56 +0200 Subject: [PATCH 24/69] Implement slow mode and recovery --- Tools/AutoClient/Configuration.cs | 3 + .../AutoClient/Modes/FolderStore/FileSaver.cs | 46 +++++++++------ .../Modes/FolderStore/FolderSaver.cs | 56 ++++++++----------- .../Modes/FolderStore/SlowModeHandler.cs | 54 ++++++++++++++++++ 4 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 Tools/AutoClient/Modes/FolderStore/SlowModeHandler.cs diff --git a/Tools/AutoClient/Configuration.cs b/Tools/AutoClient/Configuration.cs index 13aea6fd..65996333 100644 --- a/Tools/AutoClient/Configuration.cs +++ b/Tools/AutoClient/Configuration.cs @@ -59,6 +59,9 @@ namespace AutoClient "/root/codex-testnet-starter/scripts/eth_7.address" + ";" + "/root/codex-testnet-starter/scripts/eth_8.address"; + [Uniform("slowModeDelayMinutes", "smdm", "SLOWMODEDELAYMINUTES", false, "When contract failure threshold is reached, slow down process for each file by this amount of minutes.")] + public int SlowModeDelayMinutes { get; set; } = 60 * 1; + public string LogPath { get diff --git a/Tools/AutoClient/Modes/FolderStore/FileSaver.cs b/Tools/AutoClient/Modes/FolderStore/FileSaver.cs index 027065e4..448755f0 100644 --- a/Tools/AutoClient/Modes/FolderStore/FileSaver.cs +++ b/Tools/AutoClient/Modes/FolderStore/FileSaver.cs @@ -7,6 +7,11 @@ namespace AutoClient.Modes.FolderStore public interface IFileSaverEventHandler { void SaveChanges(); + } + + public interface IFileSaverResultHandler + { + void OnSuccess(); void OnFailure(); } @@ -17,16 +22,18 @@ namespace AutoClient.Modes.FolderStore private readonly Stats stats; private readonly string folderFile; private readonly FileStatus entry; - private readonly IFileSaverEventHandler handler; + private readonly IFileSaverEventHandler saveHandler; + private readonly IFileSaverResultHandler resultHandler; - public FileSaver(ILog log, LoadBalancer loadBalancer, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler handler) + public FileSaver(ILog log, LoadBalancer loadBalancer, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler saveHandler, IFileSaverResultHandler resultHandler) { this.log = log; this.loadBalancer = loadBalancer; this.stats = stats; this.folderFile = folderFile; this.entry = entry; - this.handler = handler; + this.saveHandler = saveHandler; + this.resultHandler = resultHandler; } public void Process() @@ -46,9 +53,9 @@ namespace AutoClient.Modes.FolderStore loadBalancer.DispatchOnCodex(instance => { entry.CodexNodeId = instance.Node.GetName(); - handler.SaveChanges(); + saveHandler.SaveChanges(); - var run = new FileSaverRun(log, instance, stats, folderFile, entry, handler); + var run = new FileSaverRun(log, instance, stats, folderFile, entry, saveHandler, resultHandler); run.Process(); }); } @@ -57,7 +64,7 @@ namespace AutoClient.Modes.FolderStore { loadBalancer.DispatchOnSpecificCodex(instance => { - var run = new FileSaverRun(log, instance, stats, folderFile, entry, handler); + var run = new FileSaverRun(log, instance, stats, folderFile, entry, saveHandler, resultHandler); run.Process(); }, entry.CodexNodeId); } @@ -70,17 +77,19 @@ namespace AutoClient.Modes.FolderStore private readonly Stats stats; private readonly string folderFile; private readonly FileStatus entry; - private readonly IFileSaverEventHandler handler; + private readonly IFileSaverEventHandler saveHandler; + private readonly IFileSaverResultHandler resultHandler; private readonly QuotaCheck quotaCheck; - public FileSaverRun(ILog log, CodexWrapper instance, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler handler) + public FileSaverRun(ILog log, CodexWrapper instance, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler saveHandler, IFileSaverResultHandler resultHandler) { this.log = log; this.instance = instance; this.stats = stats; this.folderFile = folderFile; this.entry = entry; - this.handler = handler; + this.saveHandler = saveHandler; + this.resultHandler = resultHandler; quotaCheck = new QuotaCheck(log, folderFile, instance); } @@ -127,7 +136,7 @@ namespace AutoClient.Modes.FolderStore Thread.Sleep(TimeSpan.FromMinutes(1.0)); } Log("Could not upload: Insufficient local storage quota."); - handler.OnFailure(); + resultHandler.OnFailure(); return false; } @@ -206,9 +215,9 @@ namespace AutoClient.Modes.FolderStore entry.BasicCid = string.Empty; stats.FailedUploads++; log.Error("Failed to upload: " + exc); - handler.OnFailure(); + resultHandler.OnFailure(); } - handler.SaveChanges(); + saveHandler.SaveChanges(); } private void CreateNewPurchase() @@ -224,17 +233,18 @@ namespace AutoClient.Modes.FolderStore WaitForStarted(request); stats.StorageRequestStats.SuccessfullyStarted++; - handler.SaveChanges(); + saveHandler.SaveChanges(); Log($"Successfully started new purchase: '{entry.PurchaseId}' for {Time.FormatDuration(request.Purchase.Duration)}"); + resultHandler.OnSuccess(); } catch (Exception exc) { entry.EncodedCid = string.Empty; entry.PurchaseId = string.Empty; - handler.SaveChanges(); + saveHandler.SaveChanges(); log.Error("Failed to start new purchase: " + exc); - handler.OnFailure(); + resultHandler.OnFailure(); } } @@ -253,7 +263,7 @@ namespace AutoClient.Modes.FolderStore throw new Exception("CID received from storage request was not protected."); } - handler.SaveChanges(); + saveHandler.SaveChanges(); Log("Saved new purchaseId: " + entry.PurchaseId); return request; } @@ -289,7 +299,7 @@ namespace AutoClient.Modes.FolderStore Log("Request failed to start. State: " + update.State); entry.EncodedCid = string.Empty; entry.PurchaseId = string.Empty; - handler.SaveChanges(); + saveHandler.SaveChanges(); return; } } @@ -297,7 +307,7 @@ namespace AutoClient.Modes.FolderStore } catch (Exception exc) { - handler.OnFailure(); + resultHandler.OnFailure(); Log($"Exception in {nameof(WaitForSubmittedToStarted)}: {exc}"); throw; } diff --git a/Tools/AutoClient/Modes/FolderStore/FolderSaver.cs b/Tools/AutoClient/Modes/FolderStore/FolderSaver.cs index 4d090e6d..d951aaee 100644 --- a/Tools/AutoClient/Modes/FolderStore/FolderSaver.cs +++ b/Tools/AutoClient/Modes/FolderStore/FolderSaver.cs @@ -11,14 +11,16 @@ namespace AutoClient.Modes.FolderStore private readonly JsonFile statusFile; private readonly FolderStatus status; private readonly BalanceChecker balanceChecker; + private readonly SlowModeHandler slowModeHandler; private int changeCounter = 0; - private int failureCount = 0; + private int saveFolderJsonCounter = 0; public FolderSaver(App app, LoadBalancer loadBalancer) { this.app = app; this.loadBalancer = loadBalancer; balanceChecker = new BalanceChecker(app); + slowModeHandler = new SlowModeHandler(app); statusFile = new JsonFile(app, Path.Combine(app.Config.FolderToStore, FolderSaverFilename)); status = statusFile.Load(); @@ -26,10 +28,11 @@ namespace AutoClient.Modes.FolderStore public void Run() { + saveFolderJsonCounter = 0; + var folderFiles = Directory.GetFiles(app.Config.FolderToStore); if (!folderFiles.Any()) throw new Exception("No files found in " + app.Config.FolderToStore); - var saveFolderJsonCounter = 0; balanceChecker.Check(); foreach (var folderFile in folderFiles) { @@ -41,35 +44,30 @@ namespace AutoClient.Modes.FolderStore SaveFile(folderFile); } - if (failureCount > 3) - { - app.Log.Error("Failure count reached threshold. Stopping..."); - app.Cts.Cancel(); - return; - } - - if (changeCounter > 1) - { - changeCounter = 0; - saveFolderJsonCounter++; - if (saveFolderJsonCounter > 5) - { - saveFolderJsonCounter = 0; - if (failureCount > 0) - { - app.Log.Log($"Failure count is reset. (Was: {failureCount})"); - failureCount = 0; - } - balanceChecker.Check(); - SaveFolderSaverJsonFile(); - } - } + slowModeHandler.Check(); + + CheckAndSaveChanges(); statusFile.Save(status); Thread.Sleep(100); } } + private void CheckAndSaveChanges() + { + if (changeCounter > 1) + { + changeCounter = 0; + saveFolderJsonCounter++; + if (saveFolderJsonCounter > 5) + { + saveFolderJsonCounter = 0; + balanceChecker.Check(); + SaveFolderSaverJsonFile(); + } + } + } + private void SaveFile(string folderFile) { var localFilename = Path.GetFileName(folderFile); @@ -114,7 +112,6 @@ namespace AutoClient.Modes.FolderStore } private const int MinCodexStorageFilesize = 262144; - private readonly Random random = new Random(); private readonly string paddingMessage = $"Codex currently requires a minimum filesize of {MinCodexStorageFilesize} bytes for datasets used in storage contracts. " + $"Anything smaller, and the erasure-coding algorithms used for data durability won't function. Therefore, we apply this padding field to make sure this " + $"file is larger than the minimal size. The following is pseudo-random: "; @@ -135,7 +132,7 @@ namespace AutoClient.Modes.FolderStore { var fixedLength = entry.Filename.PadRight(35); var prefix = $"[{fixedLength}] "; - return new FileSaver(new LogPrefixer(app.Log, prefix), loadBalancer, status.Stats, folderFile, entry, this); + return new FileSaver(new LogPrefixer(app.Log, prefix), loadBalancer, status.Stats, folderFile, entry, this, slowModeHandler); } public void SaveChanges() @@ -143,10 +140,5 @@ namespace AutoClient.Modes.FolderStore statusFile.Save(status); changeCounter++; } - - public void OnFailure() - { - failureCount++; - } } } diff --git a/Tools/AutoClient/Modes/FolderStore/SlowModeHandler.cs b/Tools/AutoClient/Modes/FolderStore/SlowModeHandler.cs new file mode 100644 index 00000000..37d3f7ee --- /dev/null +++ b/Tools/AutoClient/Modes/FolderStore/SlowModeHandler.cs @@ -0,0 +1,54 @@ +namespace AutoClient.Modes.FolderStore +{ + public class SlowModeHandler : IFileSaverResultHandler + { + private readonly App app; + private int failureCount = 0; + private bool slowMode = false; + private int recoveryCount = 0; + + public SlowModeHandler(App app) + { + this.app = app; + } + + public void OnSuccess() + { + failureCount = 0; + if (slowMode) + { + recoveryCount++; + if (recoveryCount > 3) + { + Log("Recovery limit reached. Exiting slow mode."); + slowMode = false; + failureCount = 0; + } + } + } + + public void OnFailure() + { + failureCount++; + if (failureCount > 3 && !slowMode) + { + Log("Failure limit reached. Entering slow mode."); + slowMode = true; + recoveryCount = 0; + } + } + + public void Check() + { + if (slowMode) + { + Thread.Sleep(TimeSpan.FromMinutes(app.Config.SlowModeDelayMinutes)); + } + } + + private void Log(string msg) + { + app.Log.Log(msg); + } + } +} From 3fe2827080c805acacb8685bff6d14aa46425166 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 2 May 2025 08:49:43 +0200 Subject: [PATCH 25/69] Adds randomness to price per byte per second and contract duration --- Tools/AutoClient/CodexWrapper.cs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Tools/AutoClient/CodexWrapper.cs b/Tools/AutoClient/CodexWrapper.cs index e91c3cfa..02fcca3e 100644 --- a/Tools/AutoClient/CodexWrapper.cs +++ b/Tools/AutoClient/CodexWrapper.cs @@ -7,6 +7,7 @@ namespace AutoClient public class CodexWrapper { private readonly App app; + private static readonly Random r = new Random(); public CodexWrapper(App app, ICodexNode node) { @@ -26,11 +27,11 @@ namespace AutoClient var result = Node.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { CollateralPerByte = app.Config.CollateralPerByte.TstWei(), - Duration = TimeSpan.FromMinutes(app.Config.ContractDurationMinutes), + Duration = GetDuration(), Expiry = TimeSpan.FromMinutes(app.Config.ContractExpiryMinutes), MinRequiredNumberOfNodes = Convert.ToUInt32(app.Config.NumHosts), NodeFailureTolerance = Convert.ToUInt32(app.Config.HostTolerance), - PricePerBytePerSecond = app.Config.PricePerBytePerSecond.TstWei(), + PricePerBytePerSecond = GetPricePerBytePerSecond(), ProofProbability = 15 }); return result; @@ -40,5 +41,25 @@ namespace AutoClient { return Node.GetPurchaseStatus(pid); } + + private TestToken GetPricePerBytePerSecond() + { + var i = app.Config.PricePerBytePerSecond; + i -= 100; + i += r.Next(0, 1000); + + return i.TstWei(); + } + + private TimeSpan GetDuration() + { + var i = app.Config.ContractDurationMinutes; + var day = 60 * 24; + i -= day; + i -= 10; // We don't want to accidentally hit exactly 7 days because that's the limit of the storage node availabilities. + i += r.Next(0, day * 2); + + return TimeSpan.FromMinutes(i); + } } } From 8a685ffe9a44cfbdfbf60a68a55eaa679153ca5d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 2 May 2025 10:25:18 +0200 Subject: [PATCH 26/69] 0.2.1 image with three workers. more clients and hosts --- ProjectPlugins/CodexPlugin/CodexDockerImage.cs | 3 +-- .../MarketTests/MultipleContractsTest.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index afcfc747..55301cf8 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,8 +2,7 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-1932aec-dist-tests"; - //"codexstorage/nim-codex:0.2.1-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.1-dist-tests"; public static string Override { get; set; } = string.Empty; diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 672f9eee..73b3fa91 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -5,11 +5,11 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture(6, 3, 1)] - [TestFixture(6, 4, 1)] - [TestFixture(8, 5, 1)] - [TestFixture(8, 6, 1)] - [TestFixture(8, 6, 3)] + [TestFixture(8, 3, 1)] + [TestFixture(8, 4, 1)] + [TestFixture(10, 5, 1)] + [TestFixture(10, 6, 1)] + [TestFixture(10, 6, 3)] public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest { public MultipleContractsTest(int hosts, int slots, int tolerance) @@ -25,8 +25,8 @@ namespace CodexReleaseTests.MarketTests private readonly int tolerance; protected override int NumberOfHosts => hosts; - protected override int NumberOfClients => 6; - protected override ByteSize HostAvailabilitySize => (1000 * FilesizeMb).MB(); + protected override int NumberOfClients => 8; + protected override ByteSize HostAvailabilitySize => (1000 * FilesizeMb).MB(); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); From a4994f96b8335e33de1eac8f9d06ad2e6ded5730 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Sat, 3 May 2025 08:35:34 +0200 Subject: [PATCH 27/69] Makes chainState fetch requests from chain when it sees events for requests it doesn't already know. --- .../ChainMonitor/ChainState.cs | 24 ++++++++++--------- .../CodexContractsAccess.cs | 15 +++++++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs index 25a028dc..1bb47912 100644 --- a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs @@ -199,21 +199,23 @@ namespace CodexContractsPlugin.ChainMonitor private ChainStateRequest? FindRequest(IHasRequestId request) { var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, request.RequestId)); - if (r == null) + if (r != null) return r; + + try { - var blockNumber = "unknown"; - if (request is IHasBlock blk) - { - blockNumber = blk.Block.BlockNumber.ToString(); - } - - var msg = $"Received event of type '{request.GetType()}' in block '{blockNumber}' for request by Id: '{request.RequestId}'. " + - $"Failed to find request. Request creation event not seen! (Tracker start time: {TotalSpan.From})"; - + var req = contracts.GetRequest(request.RequestId); + var state = contracts.GetRequestState(req); + var newRequest = new ChainStateRequest(log, req, state); + requests.Add(newRequest); + return newRequest; + } + catch (Exception ex) + { + var msg = "Failed to get request from chain: " + ex; log.Error(msg); handler.OnError(msg); + return null; } - return r; } private bool Equal(byte[] a, byte[] b) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index 120574b4..40ee264e 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -3,11 +3,8 @@ using CodexContractsPlugin.Marketplace; using GethPlugin; using Logging; using Nethereum.ABI; -using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; -using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Util; -using NethereumWorkflow; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Utils; @@ -28,6 +25,7 @@ namespace CodexContractsPlugin ICodexContractsEvents GetEvents(BlockInterval blockInterval); EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex); RequestState GetRequestState(Request request); + Request GetRequest(byte[] requestId); ulong GetPeriodNumber(DateTime utc); void WaitUntilNextPeriod(); ProofState GetProofState(Request storageRequest, decimal slotIndex, ulong blockNumber, ulong period); @@ -126,6 +124,17 @@ namespace CodexContractsPlugin return gethNode.Call(Deployment.MarketplaceAddress, func); } + public Request GetRequest(byte[] requestId) + { + var func = new GetRequestFunction + { + RequestId = requestId + }; + + var request = gethNode.Call(Deployment.MarketplaceAddress, func); + return request.ReturnValue1; + } + public ulong GetPeriodNumber(DateTime utc) { DateTimeOffset utco = DateTime.SpecifyKind(utc, DateTimeKind.Utc); From 809b74b8829c04987970a43c04606c4b80a1a0ae Mon Sep 17 00:00:00 2001 From: ThatBen Date: Sat, 3 May 2025 08:54:38 +0200 Subject: [PATCH 28/69] attempt to harden block finder against rpc timeouts --- Framework/BlockchainUtils/BlockTimeFinder.cs | 3 +- Framework/BlockchainUtils/BlockchainBounds.cs | 2 + Framework/NethereumWorkflow/Web3Wrapper.cs | 41 +++++++++++++------ .../MarketplaceAutoBootstrapDistTest.cs | 3 +- .../MarketTests/MultipleContractsTest.cs | 12 +++--- Tests/CodexReleaseTests/Parallelism.cs | 2 +- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Framework/BlockchainUtils/BlockTimeFinder.cs b/Framework/BlockchainUtils/BlockTimeFinder.cs index c6f10573..ee49836c 100644 --- a/Framework/BlockchainUtils/BlockTimeFinder.cs +++ b/Framework/BlockchainUtils/BlockTimeFinder.cs @@ -20,9 +20,10 @@ namespace BlockchainUtils public BlockTimeEntry Get(ulong blockNumber) { - bounds.Initialize(); var b = cache.Get(blockNumber); if (b != null) return b; + + bounds.Initialize(); return GetBlock(blockNumber); } diff --git a/Framework/BlockchainUtils/BlockchainBounds.cs b/Framework/BlockchainUtils/BlockchainBounds.cs index 27328669..32ce728f 100644 --- a/Framework/BlockchainUtils/BlockchainBounds.cs +++ b/Framework/BlockchainUtils/BlockchainBounds.cs @@ -87,6 +87,8 @@ private void AddCurrentBlock() { var currentBlockNumber = web3.GetCurrentBlockNumber(); + if (Current != null && Current.BlockNumber == currentBlockNumber) return; + var blockTime = web3.GetTimestampForBlock(currentBlockNumber); if (blockTime == null) throw new Exception("Unable to get dateTime for current block."); AddCurrentBlock(currentBlockNumber, blockTime.Value); diff --git a/Framework/NethereumWorkflow/Web3Wrapper.cs b/Framework/NethereumWorkflow/Web3Wrapper.cs index a68ad2e4..b7859685 100644 --- a/Framework/NethereumWorkflow/Web3Wrapper.cs +++ b/Framework/NethereumWorkflow/Web3Wrapper.cs @@ -19,23 +19,40 @@ namespace NethereumWorkflow public ulong GetCurrentBlockNumber() { - var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); - return Convert.ToUInt64(number.ToDecimal()); + return Retry(() => + { + var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); + return Convert.ToUInt64(number.ToDecimal()); + }); } public DateTime? GetTimestampForBlock(ulong blockNumber) { - try + return Retry(() => { - var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber))); - if (block == null) return null; - return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime; - } - catch (Exception ex) - { - log.Error("Exception while getting timestamp for block: " + ex); - return null; - } + try + { + var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber))); + if (block == null) return null; + return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime; + } + catch (Exception ex) + { + log.Error("Exception while getting timestamp for block: " + ex); + return null; + } + }); + } + + private T Retry(Func action) + { + var retry = new Retry(nameof(Web3Wrapper), + maxTimeout: TimeSpan.FromSeconds(30), + sleepAfterFail: TimeSpan.FromSeconds(3), + onFail: f => { }, + failFast: false); + + return retry.Run(action); } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index 72a56681..f22dbaf0 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -122,7 +122,8 @@ namespace CodexReleaseTests.MarketTests var result = new ChainMonitor(log, contracts, startUtc); result.Start(() => { - Assert.Fail("Failure in chain monitor."); + log.Error("Failure in chain monitor. No chain updates after this point."); + //Assert.Fail("Failure in chain monitor."); }); return result; } diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 73b3fa91..aa5329ff 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -5,11 +5,11 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture(8, 3, 1)] + //[TestFixture(8, 3, 1)] [TestFixture(8, 4, 1)] - [TestFixture(10, 5, 1)] - [TestFixture(10, 6, 1)] - [TestFixture(10, 6, 3)] + //[TestFixture(10, 5, 1)] + //[TestFixture(10, 6, 1)] + //[TestFixture(10, 6, 3)] public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest { public MultipleContractsTest(int hosts, int slots, int tolerance) @@ -33,7 +33,7 @@ namespace CodexReleaseTests.MarketTests [Test] [Combinatorial] public void MultipleContractGenerations( - [Values(10)] int numGenerations) + [Values(50)] int numGenerations) { var hosts = StartHosts(); var clients = StartClients(); @@ -96,7 +96,7 @@ namespace CodexReleaseTests.MarketTests MinRequiredNumberOfNodes = (uint)slots, NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, - ProofProbability = 20, + ProofProbability = 1, CollateralPerByte = 1.TstWei() }); } diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index eb2da742..a1b26c73 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(4)] +[assembly: LevelOfParallelism(1)] namespace CodexReleaseTests.DataTests { } From 1f21c505121f60eb73cf7c3a717b93dfabc739ed Mon Sep 17 00:00:00 2001 From: ThatBen Date: Sun, 4 May 2025 10:53:43 +0200 Subject: [PATCH 29/69] Updates api --- ProjectPlugins/CodexClient/openapi.yaml | 20 +++++++++++++++++++ .../ChainMonitor/ChainEvents.cs | 1 - 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexClient/openapi.yaml b/ProjectPlugins/CodexClient/openapi.yaml index d59cbcfa..b221db82 100644 --- a/ProjectPlugins/CodexClient/openapi.yaml +++ b/ProjectPlugins/CodexClient/openapi.yaml @@ -627,6 +627,26 @@ paths: "500": description: Well it was bad-bad + delete: + summary: "Deletes either a single block or an entire dataset from the local node." + tags: [Data] + operationId: deleteLocal + parameters: + - in: path + name: cid + required: true + schema: + $ref: "#/components/schemas/Cid" + description: Block or dataset to be deleted. + + responses: + "204": + description: Data was successfully deleted. + "400": + description: Invalid CID is specified + "500": + description: There was an error during deletion + "/data/{cid}/network": post: summary: "Download a file from the network to the local node if it's not available locally. Note: Download is performed async. Call can return before download is completed." diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs index 8c2992da..6bbfa3cc 100644 --- a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs @@ -1,5 +1,4 @@ using CodexContractsPlugin.Marketplace; -using System.Collections.Generic; using Utils; namespace CodexContractsPlugin.ChainMonitor From 1b38059559a33157897bf5795f83b8785fa54b82 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Sun, 4 May 2025 11:20:33 +0200 Subject: [PATCH 30/69] wip try get slot reserve calls --- .../NethereumWorkflow/NethereumInteraction.cs | 5 ++++ .../CodexContractsEvents.cs | 26 +++++++++++++++++++ ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- ProjectPlugins/GethPlugin/GethNode.cs | 6 +++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 3b2c609c..3407e6b9 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -143,5 +143,10 @@ namespace NethereumWorkflow var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); return blockTimeFinder.Get(number); } + + public BlockWithTransactions GetBlk(ulong number) + { + return Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(number))); + } } } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs index 6decfde6..8fb75a07 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -4,6 +4,8 @@ using GethPlugin; using Logging; using Nethereum.Contracts; using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; using Utils; namespace CodexContractsPlugin @@ -19,6 +21,7 @@ namespace CodexContractsPlugin SlotFreedEventDTO[] GetSlotFreedEvents(); SlotReservationsFullEventDTO[] GetSlotReservationsFullEvents(); ProofSubmittedEventDTO[] GetProofSubmittedEvents(); + void Do(); } public class CodexContractsEvents : ICodexContractsEvents @@ -33,10 +36,33 @@ namespace CodexContractsPlugin this.gethNode = gethNode; this.deployment = deployment; BlockInterval = blockInterval; + + Do(); } public BlockInterval BlockInterval { get; } + public void Do() + { + for (ulong i = BlockInterval.From; i <= BlockInterval.To; i++) + { + var block = gethNode.GetBlk(i); + if (block == null) return; + + foreach (var t in block.Transactions) + { + if (t == null) continue; + + var input = t.ConvertToTransactionInput(); + var aaa = t.DecodeTransactionToFunctionMessage(); + if (aaa != null) + { + var a = 0; + } + } + } + } + public Request[] GetStorageRequests() { var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index 4b190b34..ba799e60 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -10,7 +10,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "06-B9-41-E8-C8-6C-DE-01-86-83-F3-9A-E4-AC-E7-30-D9-E6-64-60-E0-21-81-9E-4E-C5-93-77-2C-71-79-14"; + private const string OpenApiYamlHash = "FD-C8-0F-19-5E-14-09-C9-05-93-17-4A-97-50-1D-7E-37-50-B2-30-B2-E6-66-37-23-FA-35-F5-AB-A0-C6-BD"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 26bf1f03..6aee2d73 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -31,6 +31,7 @@ namespace GethPlugin List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); BlockTimeEntry GetBlockForNumber(ulong number); + BlockWithTransactions GetBlk(ulong number); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -183,6 +184,11 @@ namespace GethPlugin return StartInteraction().GetBlockForNumber(number); } + public BlockWithTransactions GetBlk(ulong number) + { + return StartInteraction().GetBlk(number); + } + protected abstract NethereumInteraction StartInteraction(); } } From 69cf4283fa13c2270444d9ac055410a0d798b38b Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 6 May 2025 20:33:37 +0200 Subject: [PATCH 31/69] Adds assert all slots were fully reserved. Adds logs for calls to reserveslot when start fails. --- .../NethereumWorkflow/NethereumInteraction.cs | 2 +- .../CodexContractsEvents.cs | 32 +++-------- ProjectPlugins/GethPlugin/GethNode.cs | 20 ++++++- .../MarketTests/ContractSuccessfulTest.cs | 3 +- .../MarketplaceAutoBootstrapDistTest.cs | 55 +++++++++++++++++++ .../MarketTests/MultipleContractsTest.cs | 3 +- 6 files changed, 86 insertions(+), 29 deletions(-) diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 3407e6b9..6a34e848 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -144,7 +144,7 @@ namespace NethereumWorkflow return blockTimeFinder.Get(number); } - public BlockWithTransactions GetBlk(ulong number) + public BlockWithTransactions GetBlockWithTransactions(ulong number) { return Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(number))); } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs index 8fb75a07..3fd07634 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -21,7 +21,7 @@ namespace CodexContractsPlugin SlotFreedEventDTO[] GetSlotFreedEvents(); SlotReservationsFullEventDTO[] GetSlotReservationsFullEvents(); ProofSubmittedEventDTO[] GetProofSubmittedEvents(); - void Do(); + ReserveSlotFunction[] GetReserveSlotCalls(); } public class CodexContractsEvents : ICodexContractsEvents @@ -36,33 +36,10 @@ namespace CodexContractsPlugin this.gethNode = gethNode; this.deployment = deployment; BlockInterval = blockInterval; - - Do(); } public BlockInterval BlockInterval { get; } - public void Do() - { - for (ulong i = BlockInterval.From; i <= BlockInterval.To; i++) - { - var block = gethNode.GetBlk(i); - if (block == null) return; - - foreach (var t in block.Transactions) - { - if (t == null) continue; - - var input = t.ConvertToTransactionInput(); - var aaa = t.DecodeTransactionToFunctionMessage(); - if (aaa != null) - { - var a = 0; - } - } - } - } - public Request[] GetStorageRequests() { var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); @@ -125,6 +102,13 @@ namespace CodexContractsPlugin return events.Select(SetBlockOnEvent).ToArray(); } + public ReserveSlotFunction[] GetReserveSlotCalls() + { + var result = new List(); + gethNode.IterateFunctionCalls(BlockInterval, result.Add); + return result.ToArray(); + } + private T SetBlockOnEvent(EventLog e) where T : IHasBlock { var result = e.Event; diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 6aee2d73..9cd11ac3 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -31,7 +31,8 @@ namespace GethPlugin List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); BlockTimeEntry GetBlockForNumber(ulong number); - BlockWithTransactions GetBlk(ulong number); + void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage; + } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -186,7 +187,22 @@ namespace GethPlugin public BlockWithTransactions GetBlk(ulong number) { - return StartInteraction().GetBlk(number); + return StartInteraction().GetBlockWithTransactions(number); + } + + public void IterateFunctionCalls(BlockInterval blockRange, Action onCall) where TFunc : FunctionMessage, new() + { + var i = StartInteraction(); + for (var blkI = blockRange.From; blkI <= blockRange.To; blkI++) + { + var blk = i.GetBlockWithTransactions(blkI); + + foreach (var t in blk.Transactions) + { + var func = t.DecodeTransactionToFunctionMessage(); + if (func != null) onCall(func); + } + } } protected abstract NethereumInteraction StartInteraction(); diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 9c1ab2e0..a89059f3 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -39,8 +39,9 @@ namespace CodexReleaseTests.MarketTests request.WaitForStorageContractSubmitted(); AssertContractIsOnChain(request); + WaitUntilSlotReservationsFull(request); - request.WaitForStorageContractStarted(); + WaitForContractStarted(request); AssertContractSlotsAreFilledByHosts(request, hosts); Thread.Sleep(TimeSpan.FromSeconds(12.0)); diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index f22dbaf0..b6f624db 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -254,6 +254,28 @@ namespace CodexReleaseTests.MarketTests } } + 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(); + var calls = GetContracts().GetEvents(GetTestRunTimeRange()).GetReserveSlotCalls(); + + Log($"Request '{requestId}' failed to start. There were {calls.Length} hosts who called reserve-slot for it:"); + foreach (var c in calls) + { + Log($" - Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); + } + } + } + private TestToken GetContractFinalCost(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts) { var fills = GetOnChainSlotFills(hosts); @@ -323,6 +345,39 @@ namespace CodexReleaseTests.MarketTests }, nameof(AssertContractIsOnChain)); } + protected void WaitUntilSlotReservationsFull(IStoragePurchaseContract contract) + { + var requestId = contract.PurchaseId.ToLowerInvariant(); + var slots = contract.Purchase.MinRequiredNumberOfNodes; + + var timeout = TimeSpan.FromMinutes(1.0); + var start = DateTime.UtcNow; + var fullIndices = new List(); + + while (DateTime.UtcNow - start < timeout) + { + Thread.Sleep(TimeSpan.FromSeconds(3.0)); + + var fullEvents = GetContracts().GetEvents(GetTestRunTimeRange()).GetSlotReservationsFullEvents(); + foreach (var e in fullEvents) + { + if (e.RequestId.ToHex().ToLowerInvariant() == requestId) + { + if (!fullIndices.Contains(e.SlotIndex)) + { + fullIndices.Add(e.SlotIndex); + if (fullIndices.Count == slots) return; + } + } + } + } + + Assert.Fail( + $"Slot reservations were not full after {Time.FormatDuration(timeout)}." + + $" Slots: {slots} Filled: {string.Join(",", fullIndices.Select(i => i.ToString()))}" + ); + } + protected void AssertOnChainEvents(Action onEvents, string description) { Time.Retry(() => diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index aa5329ff..e5588efb 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -57,7 +57,8 @@ namespace CodexReleaseTests.MarketTests AssertContractIsOnChain(r); }); - All(requests, r => r.WaitForStorageContractStarted()); + All(requests, WaitUntilSlotReservationsFull); + All(requests, WaitForContractStarted); // for the time being, we're only interested in whether these contracts start. //All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); From b3d35933a8e77514094049390560fc3c4fe66f64 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 7 May 2025 15:20:24 +0200 Subject: [PATCH 32/69] Trying to log slot reservation calls --- ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- ProjectPlugins/GethPlugin/GethNode.cs | 10 +++--- .../MarketTests/ContractSuccessfulTest.cs | 1 - .../MarketplaceAutoBootstrapDistTest.cs | 33 ------------------- .../MarketTests/MultipleContractsTest.cs | 1 - 5 files changed, 7 insertions(+), 40 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index ba799e60..4b190b34 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -10,7 +10,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "FD-C8-0F-19-5E-14-09-C9-05-93-17-4A-97-50-1D-7E-37-50-B2-30-B2-E6-66-37-23-FA-35-F5-AB-A0-C6-BD"; + private const string OpenApiYamlHash = "06-B9-41-E8-C8-6C-DE-01-86-83-F3-9A-E4-AC-E7-30-D9-E6-64-60-E0-21-81-9E-4E-C5-93-77-2C-71-79-14"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 9cd11ac3..e94a5e09 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -31,8 +31,7 @@ namespace GethPlugin List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); BlockTimeEntry GetBlockForNumber(ulong number); - void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage; - + void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -199,8 +198,11 @@ namespace GethPlugin foreach (var t in blk.Transactions) { - var func = t.DecodeTransactionToFunctionMessage(); - if (func != null) onCall(func); + if (t.IsTransactionForFunctionMessage()) + { + var func = t.DecodeTransactionToFunctionMessage(); + if (func != null) onCall(func); + } } } } diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index a89059f3..51f9b79b 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -39,7 +39,6 @@ namespace CodexReleaseTests.MarketTests request.WaitForStorageContractSubmitted(); AssertContractIsOnChain(request); - WaitUntilSlotReservationsFull(request); WaitForContractStarted(request); AssertContractSlotsAreFilledByHosts(request, hosts); diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index b6f624db..146cc993 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -345,39 +345,6 @@ namespace CodexReleaseTests.MarketTests }, nameof(AssertContractIsOnChain)); } - protected void WaitUntilSlotReservationsFull(IStoragePurchaseContract contract) - { - var requestId = contract.PurchaseId.ToLowerInvariant(); - var slots = contract.Purchase.MinRequiredNumberOfNodes; - - var timeout = TimeSpan.FromMinutes(1.0); - var start = DateTime.UtcNow; - var fullIndices = new List(); - - while (DateTime.UtcNow - start < timeout) - { - Thread.Sleep(TimeSpan.FromSeconds(3.0)); - - var fullEvents = GetContracts().GetEvents(GetTestRunTimeRange()).GetSlotReservationsFullEvents(); - foreach (var e in fullEvents) - { - if (e.RequestId.ToHex().ToLowerInvariant() == requestId) - { - if (!fullIndices.Contains(e.SlotIndex)) - { - fullIndices.Add(e.SlotIndex); - if (fullIndices.Count == slots) return; - } - } - } - } - - Assert.Fail( - $"Slot reservations were not full after {Time.FormatDuration(timeout)}." + - $" Slots: {slots} Filled: {string.Join(",", fullIndices.Select(i => i.ToString()))}" - ); - } - protected void AssertOnChainEvents(Action onEvents, string description) { Time.Retry(() => diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index e5588efb..df71eb3b 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -57,7 +57,6 @@ namespace CodexReleaseTests.MarketTests AssertContractIsOnChain(r); }); - All(requests, WaitUntilSlotReservationsFull); All(requests, WaitForContractStarted); // for the time being, we're only interested in whether these contracts start. From 5e62c3520c4ce71f8474cca6fe9ac5557cc12fe2 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 14 May 2025 11:45:15 +0200 Subject: [PATCH 33/69] Prevents downloading of crash log in retry loop --- Framework/KubernetesWorkflow/ContainerCrashWatcher.cs | 9 +++++---- ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Framework/KubernetesWorkflow/ContainerCrashWatcher.cs b/Framework/KubernetesWorkflow/ContainerCrashWatcher.cs index f4f181aa..ff4dcc1e 100644 --- a/Framework/KubernetesWorkflow/ContainerCrashWatcher.cs +++ b/Framework/KubernetesWorkflow/ContainerCrashWatcher.cs @@ -14,6 +14,7 @@ namespace KubernetesWorkflow private CancellationTokenSource cts; private Task? worker; private Exception? workerException; + private bool hasCrashed = false; public ContainerCrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace) { @@ -47,10 +48,7 @@ namespace KubernetesWorkflow public bool HasCrashed() { - using var client = new Kubernetes(config); - var result = HasContainerBeenRestarted(client); - if (result) DownloadCrashedContainerLogs(client); - return result; + return hasCrashed; } private void Worker() @@ -72,6 +70,9 @@ namespace KubernetesWorkflow { if (HasContainerBeenRestarted(client)) { + hasCrashed = true; + cts.Cancel(); + DownloadCrashedContainerLogs(client); return; } diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index 4b190b34..ba799e60 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -10,7 +10,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "06-B9-41-E8-C8-6C-DE-01-86-83-F3-9A-E4-AC-E7-30-D9-E6-64-60-E0-21-81-9E-4E-C5-93-77-2C-71-79-14"; + private const string OpenApiYamlHash = "FD-C8-0F-19-5E-14-09-C9-05-93-17-4A-97-50-1D-7E-37-50-B2-30-B2-E6-66-37-23-FA-35-F5-AB-A0-C6-BD"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; From fe2d7484dbfbec98b01f2522ae96f3f2a528364a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 14 May 2025 14:11:36 +0200 Subject: [PATCH 34/69] Updates API --- ProjectPlugins/CodexClient/openapi.yaml | 1 + ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexClient/openapi.yaml b/ProjectPlugins/CodexClient/openapi.yaml index b221db82..fae38467 100644 --- a/ProjectPlugins/CodexClient/openapi.yaml +++ b/ProjectPlugins/CodexClient/openapi.yaml @@ -205,6 +205,7 @@ components: required: - id - totalRemainingCollateral + - freeSize allOf: - $ref: "#/components/schemas/SalesAvailability" - type: object diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index ba799e60..d4c1a615 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -10,7 +10,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "FD-C8-0F-19-5E-14-09-C9-05-93-17-4A-97-50-1D-7E-37-50-B2-30-B2-E6-66-37-23-FA-35-F5-AB-A0-C6-BD"; + private const string OpenApiYamlHash = "2F-9D-82-3C-F0-2F-D3-C9-72-C3-F2-6E-BD-C3-63-F5-67-62-D1-03-B6-60-75-31-22-DF-3F-63-A2-8D-AA-4B"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; From 18b1f73025a4142a40c0242c1968879fb2d3450a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 14 May 2025 15:01:21 +0200 Subject: [PATCH 35/69] Enables console output for test logs --- Tests/DistTestCore/Global.cs | 9 +++++++-- Tests/DistTestCore/Logs/BaseTestLog.cs | 18 +----------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Tests/DistTestCore/Global.cs b/Tests/DistTestCore/Global.cs index 4ba1374b..ddd7a5cc 100644 --- a/Tests/DistTestCore/Global.cs +++ b/Tests/DistTestCore/Global.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using Core; using Logging; @@ -33,7 +34,9 @@ namespace DistTestCore { try { - Stopwatch.Measure(log, "Global setup", () => + Trace.Listeners.Add(new ConsoleTraceListener()); + + Logging.Stopwatch.Measure(log, "Global setup", () => { globalEntryPoint.Announce(); globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true); @@ -55,6 +58,8 @@ namespace DistTestCore deleteTrackedFiles: true, waitTillDone: true ); + + Trace.Flush(); } } } diff --git a/Tests/DistTestCore/Logs/BaseTestLog.cs b/Tests/DistTestCore/Logs/BaseTestLog.cs index e670e0c1..51775512 100644 --- a/Tests/DistTestCore/Logs/BaseTestLog.cs +++ b/Tests/DistTestCore/Logs/BaseTestLog.cs @@ -59,24 +59,8 @@ namespace DistTestCore.Logs protected static ILog CreateMainLog(string fullName, string name) { - ILog log = new FileLog(fullName); - log = ApplyConsoleOutput(log); - return log; - } - - private static ILog ApplyConsoleOutput(ILog log) - { - // If we're running as a release test, we'll split the log output - // to the console as well. - - var testType = Environment.GetEnvironmentVariable("TEST_TYPE"); - if (string.IsNullOrEmpty(testType) || testType.ToLowerInvariant() != "release-tests") - { - return log; - } - return new LogSplitter( - log, + new FileLog(fullName), new ConsoleLog() ); } From c3f3ef3f960fdb53352bc1bff323cc8ca6f6b0ad Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 14 May 2025 15:40:22 +0200 Subject: [PATCH 36/69] Reduce concurrency for cloud runner because volume claims are more restricted there --- Tests/CodexReleaseTests/Parallelism.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexReleaseTests/Parallelism.cs b/Tests/CodexReleaseTests/Parallelism.cs index d589af32..44731c5e 100644 --- a/Tests/CodexReleaseTests/Parallelism.cs +++ b/Tests/CodexReleaseTests/Parallelism.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -[assembly: LevelOfParallelism(10)] +[assembly: LevelOfParallelism(2)] namespace CodexReleaseTests { } From a85caae203b6fa401d6dfaa86a3d586c787cb9b7 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 10:19:07 +0200 Subject: [PATCH 37/69] Setting up contract tracer --- .../CodexContractsEvents.cs | 8 +- .../Marketplace/Customizations.cs | 17 +- ProjectPlugins/GethPlugin/GethNode.cs | 10 +- .../MarketplaceAutoBootstrapDistTest.cs | 2 +- TraceContract/Config.cs | 11 ++ TraceContract/Output.cs | 18 ++ TraceContract/Program.cs | 169 ++++++++++++++++++ TraceContract/TraceContract.csproj | 16 ++ cs-codex-dist-testing.sln | 7 + 9 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 TraceContract/Config.cs create mode 100644 TraceContract/Output.cs create mode 100644 TraceContract/Program.cs create mode 100644 TraceContract/TraceContract.csproj diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs index 3fd07634..e2d8a11a 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -4,8 +4,6 @@ using GethPlugin; using Logging; using Nethereum.Contracts; using Nethereum.Hex.HexTypes; -using Nethereum.RPC.Eth.DTOs; -using Nethereum.Web3; using Utils; namespace CodexContractsPlugin @@ -105,7 +103,11 @@ namespace CodexContractsPlugin public ReserveSlotFunction[] GetReserveSlotCalls() { var result = new List(); - gethNode.IterateFunctionCalls(BlockInterval, result.Add); + gethNode.IterateFunctionCalls(BlockInterval, (b, fn) => + { + fn.Block = b; + result.Add(fn); + }); return result.ToArray(); } diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index a93df5d1..222dc631 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -15,6 +15,11 @@ namespace CodexContractsPlugin.Marketplace byte[] RequestId { get; set; } } + public interface IHasSlotIndex + { + ulong SlotIndex { get; set; } + } + public partial class Request : RequestBase, IHasBlock, IHasRequestId { [JsonIgnore] @@ -51,20 +56,20 @@ namespace CodexContractsPlugin.Marketplace public BlockTimeEntry Block { get; set; } } - public partial class SlotFilledEventDTO : IHasBlock, IHasRequestId + public partial class SlotFilledEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex { [JsonIgnore] public BlockTimeEntry Block { get; set; } public EthAddress Host { get; set; } } - public partial class SlotFreedEventDTO : IHasBlock, IHasRequestId + public partial class SlotFreedEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex { [JsonIgnore] public BlockTimeEntry Block { get; set; } } - public partial class SlotReservationsFullEventDTO : IHasBlock, IHasRequestId + public partial class SlotReservationsFullEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex { [JsonIgnore] public BlockTimeEntry Block { get; set; } @@ -75,5 +80,11 @@ namespace CodexContractsPlugin.Marketplace [JsonIgnore] public BlockTimeEntry Block { get; set; } } + + public partial class ReserveSlotFunction : IHasBlock, IHasRequestId, IHasSlotIndex + { + [JsonIgnore] + public BlockTimeEntry Block { get; set; } + } } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index e94a5e09..cfd0af37 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -31,7 +31,7 @@ namespace GethPlugin List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); BlockTimeEntry GetBlockForNumber(ulong number); - void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage, new(); + void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -189,7 +189,7 @@ namespace GethPlugin return StartInteraction().GetBlockWithTransactions(number); } - public void IterateFunctionCalls(BlockInterval blockRange, Action onCall) where TFunc : FunctionMessage, new() + public void IterateFunctionCalls(BlockInterval blockRange, Action onCall) where TFunc : FunctionMessage, new() { var i = StartInteraction(); for (var blkI = blockRange.From; blkI <= blockRange.To; blkI++) @@ -201,7 +201,11 @@ namespace GethPlugin if (t.IsTransactionForFunctionMessage()) { var func = t.DecodeTransactionToFunctionMessage(); - if (func != null) onCall(func); + if (func != null) + { + var b = GetBlockForNumber(blkI); + onCall(b, func); + } } } } diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index 146cc993..39d2e1b3 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -271,7 +271,7 @@ namespace CodexReleaseTests.MarketTests Log($"Request '{requestId}' failed to start. There were {calls.Length} hosts who called reserve-slot for it:"); foreach (var c in calls) { - Log($" - Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); + Log($" - {c.Block.Utc} Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); } } } diff --git a/TraceContract/Config.cs b/TraceContract/Config.cs new file mode 100644 index 00000000..8f52e7cf --- /dev/null +++ b/TraceContract/Config.cs @@ -0,0 +1,11 @@ +namespace TraceContract +{ + public class Config + { + public string RpcEndpoint { get; } = "https://rpc.testnet.codex.storage"; + public int GethPort { get; } = 443; + public string MarketplaceAddress { get; } = "0xDB2908d724a15d05c0B6B8e8441a8b36E67476d3"; + public string TokenAddress { get; } = "0x34a22f3911De437307c6f4485931779670f78764"; + public string Abi { get; } = @"[{""inputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":""configuration"",""type"":""tuple""},{""internalType"":""contract IERC20"",""name"":""token_"",""type"":""address""},{""internalType"":""contract IGroth16Verifier"",""name"":""verifier"",""type"":""address""}],""stateMutability"":""nonpayable"",""type"":""constructor""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""ProofSubmitted"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestCancelled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFailed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFulfilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFreed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""indexed"":false,""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""indexed"":false,""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""}],""name"":""StorageRequested"",""type"":""event""},{""inputs"":[],""name"":""config"",""outputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""fillSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""freeSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getActiveSlot"",""outputs"":[{""components"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""internalType"":""struct Marketplace.ActiveSlot"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getChallenge"",""outputs"":[{""internalType"":""bytes32"",""name"":"""",""type"":""bytes32""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getHost"",""outputs"":[{""internalType"":""address"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getPointer"",""outputs"":[{""internalType"":""uint8"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""getRequest"",""outputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""isProofRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""},{""internalType"":""Periods.Period"",""name"":""period"",""type"":""uint256""}],""name"":""markProofAsMissing"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""missingProofs"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""myRequests"",""outputs"":[{""internalType"":""RequestId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""mySlots"",""outputs"":[{""internalType"":""SlotId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestEnd"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestExpiry"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestState"",""outputs"":[{""internalType"":""enum RequestState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""}],""name"":""requestStorage"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""slotState"",""outputs"":[{""internalType"":""enum SlotState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""submitProof"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[],""name"":""token"",""outputs"":[{""internalType"":""contract IERC20"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""willProofBeRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""withdrawFunds"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""}]"; + } +} diff --git a/TraceContract/Output.cs b/TraceContract/Output.cs new file mode 100644 index 00000000..124b2796 --- /dev/null +++ b/TraceContract/Output.cs @@ -0,0 +1,18 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.Marketplace; + +namespace TraceContract +{ + public class Output + { + public void LogRequest(Request request) + { + throw new NotImplementedException(); + } + + public void LogEventOrCall(T[] calls) where T : IHasRequestId, IHasBlock + { + throw new NotImplementedException(); + } + } +} diff --git a/TraceContract/Program.cs b/TraceContract/Program.cs new file mode 100644 index 00000000..41c4cfd7 --- /dev/null +++ b/TraceContract/Program.cs @@ -0,0 +1,169 @@ +using BlockchainUtils; +using CodexContractsPlugin; +using CodexContractsPlugin.Marketplace; +using Core; +using GethPlugin; +using Logging; +using Nethereum.Hex.HexConvertors.Extensions; +using Utils; + +namespace TraceContract +{ + public class Input + { + public string PurchaseId { get; } = "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + } + + public class Program + { + public static void Main(string[] args) + { + var p = new Program(); + p.Run(); + } + + private readonly ILog log = new ConsoleLog(); + private readonly Input input = new(); + private readonly Output output = new(); + private readonly Config config = new(); + + private void Run() + { + try + { + TracePurchase(); + } + catch (Exception exc) + { + log.Error(exc.ToString()); + } + } + + private void TracePurchase() + { + Log("Setting up..."); + var contracts = ConnectCodexContracts(); + + var chainTracer = new ChainTracer(log, contracts, input, output); + chainTracer.TraceChainTimeline(); + + Log("Done"); + } + + private ICodexContracts ConnectCodexContracts() + { + ProjectPlugin.Load(); + ProjectPlugin.Load(); + + var entryPoint = new EntryPoint(log, new KubernetesWorkflow.Configuration(null, TimeSpan.FromMinutes(1.0), TimeSpan.FromSeconds(10.0), "_Unused!_"), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + entryPoint.Announce(); + var ci = entryPoint.CreateInterface(); + + var account = EthAccountGenerator.GenerateNew(); + var blockCache = new BlockCache(); + var geth = new CustomGethNode(log, blockCache, config.RpcEndpoint, config.GethPort, account.PrivateKey); + + var deployment = new CodexContractsDeployment( + config: new CodexContractsPlugin.Marketplace.MarketplaceConfig(), + marketplaceAddress: config.MarketplaceAddress, + abi: config.Abi, + tokenAddress: config.TokenAddress + ); + return ci.WrapCodexContractsDeployment(geth, deployment); + } + + private void Log(string msg) + { + log.Log(msg); + } + } + + public class ChainTracer + { + private readonly ILog log; + private readonly ICodexContracts contracts; + private readonly Input input; + private readonly Output output; + + public ChainTracer(ILog log, ICodexContracts contracts, Input input, Output output) + { + this.log = log; + this.contracts = contracts; + this.input = input; + this.output = output; + } + + public void TraceChainTimeline() + { + var request = GetRequest(); + if (request == null) throw new Exception("Failed to find the purchase in the last week of transactions."); + output.LogRequest(request); + + var requestTimeRange = new TimeRange(request.Block.Utc.AddMinutes(-1.0), DateTime.UtcNow); + var events = contracts.GetEvents(requestTimeRange); + + // Log calls to reserve slot for request + var calls = Filter(events.GetReserveSlotCalls()); + output.LogEventOrCall(calls); + + // log all events. + var fulls = events.GetSlotReservationsFullEvents(); + output.LogEventOrCall(Filter(fulls)); + + } + + private T[] Filter(T[] calls) where T : IHasRequestId + { + return calls.Where(c => IsThisRequest(c.RequestId)).ToArray(); + } + + private Request? GetRequest() + { + var request = FindRequest(LastHour()); + if (request == null) request = FindRequest(LastDay()); + if (request == null) request = FindRequest(LastWeek()); + return request; + } + + private Request? FindRequest(TimeRange timeRange) + { + var events = contracts.GetEvents(timeRange); + var requests = events.GetStorageRequests(); + + foreach (var r in requests) + { + if (IsThisRequest(r.RequestId)) + { + return r; + } + } + + return null; + } + + private bool IsThisRequest(byte[] requestId) + { + return requestId.ToHex().ToLowerInvariant() == input.PurchaseId.ToLowerInvariant(); + } + + private TimeRange LastHour() + { + return new TimeRange(DateTime.UtcNow.AddHours(-1.0), DateTime.UtcNow); + } + + private TimeRange LastDay() + { + return new TimeRange(DateTime.UtcNow.AddDays(-1.0), DateTime.UtcNow); + } + + private TimeRange LastWeek() + { + return new TimeRange(DateTime.UtcNow.AddDays(-7.0), DateTime.UtcNow); + } + + private void Log(string msg) + { + log.Log(msg); + } + } +} diff --git a/TraceContract/TraceContract.csproj b/TraceContract/TraceContract.csproj new file mode 100644 index 00000000..3bcdeedb --- /dev/null +++ b/TraceContract/TraceContract.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index 9b1c0977..18dd9a2e 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -86,6 +86,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexClient", "ProjectPlugi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUtils", "Framework\WebUtils\WebUtils.csproj", "{372C9E5D-5453-4D45-9948-E9324E21AD65}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceContract", "TraceContract\TraceContract.csproj", "{6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -232,6 +234,10 @@ Global {372C9E5D-5453-4D45-9948-E9324E21AD65}.Debug|Any CPU.Build.0 = Debug|Any CPU {372C9E5D-5453-4D45-9948-E9324E21AD65}.Release|Any CPU.ActiveCfg = Release|Any CPU {372C9E5D-5453-4D45-9948-E9324E21AD65}.Release|Any CPU.Build.0 = Release|Any CPU + {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -272,6 +278,7 @@ Global {4648B5AA-A0A7-44BA-89BC-2FD57370943C} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {9AF12703-29AF-416D-9781-204223D6D0E5} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124} {372C9E5D-5453-4D45-9948-E9324E21AD65} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} + {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 8776591094f6e1649ac6fc3b7e03539c0ba17659 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 12:47:36 +0200 Subject: [PATCH 38/69] wip --- TraceContract/Output.cs | 56 ++++++++++++++- TraceContract/Program.cs | 151 +++++++++++++++++++++++++++++++++------ 2 files changed, 184 insertions(+), 23 deletions(-) diff --git a/TraceContract/Output.cs b/TraceContract/Output.cs index 124b2796..6a731e0f 100644 --- a/TraceContract/Output.cs +++ b/TraceContract/Output.cs @@ -1,16 +1,66 @@ -using CodexContractsPlugin; +using System.Numerics; +using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; +using Logging; +using Utils; namespace TraceContract { public class Output { - public void LogRequest(Request request) + private readonly ILog log; + + public Output(ILog log) + { + this.log = log; + } + + public void LogRequestCreated(RequestEvent requestEvent) { throw new NotImplementedException(); } - public void LogEventOrCall(T[] calls) where T : IHasRequestId, IHasBlock + public void LogRequestCancelled(RequestEvent requestEvent) + { + throw new NotImplementedException(); + } + + public void LogRequestFailed(RequestEvent requestEvent) + { + throw new NotImplementedException(); + } + + public void LogRequestFinished(RequestEvent requestEvent) + { + throw new NotImplementedException(); + } + + public void LogRequestStarted(RequestEvent requestEvent) + { + throw new NotImplementedException(); + } + + public void LogSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + throw new NotImplementedException(); + } + + public void LogSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + throw new NotImplementedException(); + } + + public void LogSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) + { + throw new NotImplementedException(); + } + + public void LogReserveSlotCalls(ReserveSlotFunction[] reserveSlotFunctions) + { + throw new NotImplementedException(); + } + + public void WriteContractEvents() { throw new NotImplementedException(); } diff --git a/TraceContract/Program.cs b/TraceContract/Program.cs index 41c4cfd7..eabb7e16 100644 --- a/TraceContract/Program.cs +++ b/TraceContract/Program.cs @@ -1,5 +1,7 @@ -using BlockchainUtils; +using System.Numerics; +using BlockchainUtils; using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; using Core; using GethPlugin; @@ -11,7 +13,12 @@ namespace TraceContract { public class Input { - public string PurchaseId { get; } = "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + public string PurchaseId { get; } = + // expired: + //"a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + + // started: + "066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; } public class Program @@ -24,8 +31,13 @@ namespace TraceContract private readonly ILog log = new ConsoleLog(); private readonly Input input = new(); - private readonly Output output = new(); private readonly Config config = new(); + private readonly Output output; + + public Program() + { + output = new(log); + } private void Run() { @@ -45,7 +57,7 @@ namespace TraceContract var contracts = ConnectCodexContracts(); var chainTracer = new ChainTracer(log, contracts, input, output); - chainTracer.TraceChainTimeline(); + var requestTimeRange = chainTracer.TraceChainTimeline(); Log("Done"); } @@ -93,26 +105,34 @@ namespace TraceContract this.output = output; } - public void TraceChainTimeline() + public TimeRange TraceChainTimeline() { var request = GetRequest(); if (request == null) throw new Exception("Failed to find the purchase in the last week of transactions."); - output.LogRequest(request); - var requestTimeRange = new TimeRange(request.Block.Utc.AddMinutes(-1.0), DateTime.UtcNow); - var events = contracts.GetEvents(requestTimeRange); + var utc = request.Block.Utc.AddMinutes(-1.0); + var tracker = new ChainRequestTracker(output, input.PurchaseId); + var ignoreLog = new NullLog(); + var chainState = new ChainState(ignoreLog, contracts, tracker, utc, false); - // Log calls to reserve slot for request - var calls = Filter(events.GetReserveSlotCalls()); - output.LogEventOrCall(calls); + while (!tracker.IsFinished) + { + utc += TimeSpan.FromHours(1.0); + chainState.Update(utc); + } - // log all events. - var fulls = events.GetSlotReservationsFullEvents(); - output.LogEventOrCall(Filter(fulls)); + var requestTimeline = new TimeRange(request.Block.Utc.AddMinutes(-1.0), tracker.FinishUtc.AddMinutes(1.0)); + // For this timeline, we log all the calls to reserve-slot. + var events = contracts.GetEvents(requestTimeline); + output.LogReserveSlotCalls(Filter(events.GetReserveSlotCalls())); + + output.WriteContractEvents(); + + return requestTimeline; } - private T[] Filter(T[] calls) where T : IHasRequestId + private ReserveSlotFunction[] Filter(ReserveSlotFunction[] calls) { return calls.Where(c => IsThisRequest(c.RequestId)).ToArray(); } @@ -146,24 +166,115 @@ namespace TraceContract return requestId.ToHex().ToLowerInvariant() == input.PurchaseId.ToLowerInvariant(); } - private TimeRange LastHour() + private static TimeRange LastHour() { return new TimeRange(DateTime.UtcNow.AddHours(-1.0), DateTime.UtcNow); } - private TimeRange LastDay() + private static TimeRange LastDay() { return new TimeRange(DateTime.UtcNow.AddDays(-1.0), DateTime.UtcNow); } - private TimeRange LastWeek() + private static TimeRange LastWeek() { return new TimeRange(DateTime.UtcNow.AddDays(-7.0), DateTime.UtcNow); } + } - private void Log(string msg) + public class ChainRequestTracker : IChainStateChangeHandler + { + private readonly string requestId; + private readonly Output output; + + public ChainRequestTracker(Output output, string requestId) { - log.Log(msg); + this.requestId = requestId.ToLowerInvariant(); + this.output = output; + } + + public bool IsFinished { get; private set; } = false; + public DateTime FinishUtc { get; private set; } = DateTime.MinValue; + + public void OnError(string msg) + { + } + + public void OnNewRequest(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) output.LogRequestCreated(requestEvent); + } + + public void OnProofSubmitted(BlockTimeEntry block, string id) + { + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestCancelled(requestEvent); + } + } + + public void OnRequestFailed(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestFailed(requestEvent); + } + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestFinished(requestEvent); + } + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + output.LogRequestStarted(requestEvent); + } + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotFilled(requestEvent, host, slotIndex); + } + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotFreed(requestEvent, slotIndex); + } + } + + public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotReservationsFull(requestEvent, slotIndex); + } + } + + private bool IsMyRequest(RequestEvent requestEvent) + { + return requestId == requestEvent.Request.Request.Id.ToLowerInvariant(); } } + } From 9e9b147b68c1ebaea079c754c6e43e3a77f8c2a9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 14:16:33 +0200 Subject: [PATCH 39/69] wip log downloading --- Framework/KubernetesWorkflow/LogHandler.cs | 4 +- Framework/Logging/BaseLog.cs | 2 +- Framework/Logging/LogFile.cs | 14 +- Framework/Logging/LogPrefixer.cs | 11 +- Framework/Logging/TimestampPrefixer.cs | 16 ++ .../MetricsPlugin/MetricsDownloader.cs | 4 +- .../ElasticSearchLogDownloader.cs | 2 +- Tests/DistTestCore/Logs/BaseTestLog.cs | 2 +- TraceContract/ChainRequestTracker.cs | 103 +++++++ TraceContract/ChainTracer.cs | 119 ++++++++ TraceContract/Config.cs | 36 +++ TraceContract/ElasticSearchLogDownloader.cs | 242 +++++++++++++++++ TraceContract/Input.cs | 13 + TraceContract/Output.cs | 79 +++++- TraceContract/Program.cs | 254 +++--------------- 15 files changed, 647 insertions(+), 254 deletions(-) create mode 100644 Framework/Logging/TimestampPrefixer.cs create mode 100644 TraceContract/ChainRequestTracker.cs create mode 100644 TraceContract/ChainTracer.cs create mode 100644 TraceContract/ElasticSearchLogDownloader.cs create mode 100644 TraceContract/Input.cs diff --git a/Framework/KubernetesWorkflow/LogHandler.cs b/Framework/KubernetesWorkflow/LogHandler.cs index 44adcc05..b91a61f9 100644 --- a/Framework/KubernetesWorkflow/LogHandler.cs +++ b/Framework/KubernetesWorkflow/LogHandler.cs @@ -33,7 +33,7 @@ namespace KubernetesWorkflow sourceLog.Log(msg); LogFile.Write(msg); - LogFile.WriteRaw(description); + LogFile.Write(description); } public LogFile LogFile { get; } @@ -43,7 +43,7 @@ namespace KubernetesWorkflow if (line.Contains("Received JSON-RPC response")) return; if (line.Contains("object field not marked with serialize, skipping")) return; - LogFile.WriteRaw(line); + LogFile.Write(line); } } } diff --git a/Framework/Logging/BaseLog.cs b/Framework/Logging/BaseLog.cs index 56ff66ec..217b76b7 100644 --- a/Framework/Logging/BaseLog.cs +++ b/Framework/Logging/BaseLog.cs @@ -65,7 +65,7 @@ namespace Logging public void Raw(string message) { - LogFile.WriteRaw(message); + LogFile.Write(message); } public virtual void AddStringReplace(string from, string to) diff --git a/Framework/Logging/LogFile.cs b/Framework/Logging/LogFile.cs index 2ec94be0..15d2cbfc 100644 --- a/Framework/Logging/LogFile.cs +++ b/Framework/Logging/LogFile.cs @@ -1,6 +1,4 @@ -using Utils; - -namespace Logging +namespace Logging { public class LogFile { @@ -16,11 +14,6 @@ namespace Logging public string Filename { get; private set; } public void Write(string message) - { - WriteRaw($"{GetTimestamp()} {message}"); - } - - public void WriteRaw(string message) { try { @@ -50,11 +43,6 @@ namespace Logging } } - private static string GetTimestamp() - { - return $"[{Time.FormatTimestamp(DateTime.UtcNow)}]"; - } - private void EnsurePathExists(string filename) { var path = new FileInfo(filename).Directory!.FullName; diff --git a/Framework/Logging/LogPrefixer.cs b/Framework/Logging/LogPrefixer.cs index f0f303d6..30c6524d 100644 --- a/Framework/Logging/LogPrefixer.cs +++ b/Framework/Logging/LogPrefixer.cs @@ -24,17 +24,17 @@ public void Debug(string message = "", int skipFrames = 0) { - backingLog.Debug(Prefix + message, skipFrames); + backingLog.Debug(GetPrefix() + message, skipFrames); } public void Error(string message) { - backingLog.Error(Prefix + message); + backingLog.Error(GetPrefix() + message); } public void Log(string message) { - backingLog.Log(Prefix + message); + backingLog.Log(GetPrefix() + message); } public void AddStringReplace(string from, string to) @@ -51,5 +51,10 @@ { return backingLog.GetFullName(); } + + protected virtual string GetPrefix() + { + return Prefix; + } } } diff --git a/Framework/Logging/TimestampPrefixer.cs b/Framework/Logging/TimestampPrefixer.cs new file mode 100644 index 00000000..bc7c3843 --- /dev/null +++ b/Framework/Logging/TimestampPrefixer.cs @@ -0,0 +1,16 @@ +using Utils; + +namespace Logging +{ + public class TimestampPrefixer : LogPrefixer + { + public TimestampPrefixer(ILog backingLog) : base(backingLog) + { + } + + protected override string GetPrefix() + { + return $"[{Time.FormatTimestamp(DateTime.UtcNow)}]"; + } + } +} diff --git a/ProjectPlugins/MetricsPlugin/MetricsDownloader.cs b/ProjectPlugins/MetricsPlugin/MetricsDownloader.cs index 507b1ba7..7e1548b2 100644 --- a/ProjectPlugins/MetricsPlugin/MetricsDownloader.cs +++ b/ProjectPlugins/MetricsPlugin/MetricsDownloader.cs @@ -28,11 +28,11 @@ namespace MetricsPlugin var file = log.CreateSubfile("csv"); log.Log($"Downloading metrics for {nodeName} to file {file.Filename}"); - file.WriteRaw(string.Join(",", headers)); + file.Write(string.Join(",", headers)); foreach (var pair in map) { - file.WriteRaw(string.Join(",", new[] { FormatTimestamp(pair.Key) }.Concat(pair.Value))); + file.Write(string.Join(",", new[] { FormatTimestamp(pair.Key) }.Concat(pair.Value))); } return file; diff --git a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs index e198650c..5f1e968c 100644 --- a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs +++ b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs @@ -199,7 +199,7 @@ namespace ContinuousTests private void WriteEntryToFile(LogQueueEntry currentEntry) { - targetFile.WriteRaw(currentEntry.Message); + targetFile.Write(currentEntry.Message); } private void DeleteOldEntries(ulong wantedNumber) diff --git a/Tests/DistTestCore/Logs/BaseTestLog.cs b/Tests/DistTestCore/Logs/BaseTestLog.cs index 51775512..87df6383 100644 --- a/Tests/DistTestCore/Logs/BaseTestLog.cs +++ b/Tests/DistTestCore/Logs/BaseTestLog.cs @@ -8,7 +8,7 @@ namespace DistTestCore.Logs protected BaseTestLog(ILog backingLog, string deployId) { - this.backingLog = backingLog; + this.backingLog = new TimestampPrefixer(backingLog); DeployId = deployId; } diff --git a/TraceContract/ChainRequestTracker.cs b/TraceContract/ChainRequestTracker.cs new file mode 100644 index 00000000..2e313ae8 --- /dev/null +++ b/TraceContract/ChainRequestTracker.cs @@ -0,0 +1,103 @@ +using System.Numerics; +using BlockchainUtils; +using CodexContractsPlugin.ChainMonitor; +using Utils; + +namespace TraceContract +{ + public class ChainRequestTracker : IChainStateChangeHandler + { + private readonly string requestId; + private readonly Output output; + + public ChainRequestTracker(Output output, string requestId) + { + this.requestId = requestId.ToLowerInvariant(); + this.output = output; + } + + public bool IsFinished { get; private set; } = false; + public DateTime FinishUtc { get; private set; } = DateTime.MinValue; + + public void OnError(string msg) + { + } + + public void OnNewRequest(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) output.LogRequestCreated(requestEvent); + } + + public void OnProofSubmitted(BlockTimeEntry block, string id) + { + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestCancelled(requestEvent); + } + } + + public void OnRequestFailed(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestFailed(requestEvent); + } + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + IsFinished = true; + FinishUtc = requestEvent.Block.Utc; + output.LogRequestFinished(requestEvent); + } + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + if (IsMyRequest(requestEvent)) + { + output.LogRequestStarted(requestEvent); + } + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotFilled(requestEvent, host, slotIndex); + } + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotFreed(requestEvent, slotIndex); + } + } + + public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) + { + if (IsMyRequest(requestEvent)) + { + output.LogSlotReservationsFull(requestEvent, slotIndex); + } + } + + private bool IsMyRequest(RequestEvent requestEvent) + { + return requestId == requestEvent.Request.Request.Id.ToLowerInvariant(); + } + } + +} diff --git a/TraceContract/ChainTracer.cs b/TraceContract/ChainTracer.cs new file mode 100644 index 00000000..c678667a --- /dev/null +++ b/TraceContract/ChainTracer.cs @@ -0,0 +1,119 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; +using CodexContractsPlugin.Marketplace; +using Logging; +using Nethereum.Hex.HexConvertors.Extensions; +using Utils; + +namespace TraceContract +{ + public class ChainTracer + { + private readonly ILog log; + private readonly ICodexContracts contracts; + private readonly Input input; + private readonly Output output; + + public ChainTracer(ILog log, ICodexContracts contracts, Input input, Output output) + { + this.log = log; + this.contracts = contracts; + this.input = input; + this.output = output; + } + + public TimeRange TraceChainTimeline() + { + log.Log("Querying blockchain..."); + var request = GetRequest(); + if (request == null) throw new Exception("Failed to find the purchase in the last week of transactions."); + + log.Log($"Request started at {request.Block.Utc}"); + var contractEnd = RunToContractEnd(request); + + var requestTimeline = new TimeRange(request.Block.Utc.AddMinutes(-1.0), contractEnd.AddMinutes(1.0)); + log.Log($"Request timeline: {requestTimeline.From} -> {requestTimeline.To}"); + + // For this timeline, we log all the calls to reserve-slot. + var events = contracts.GetEvents(requestTimeline); + output.LogReserveSlotCalls(Filter(events.GetReserveSlotCalls())); + + log.Log("Writing blockchain output..."); + output.WriteContractEvents(); + + return requestTimeline; + } + + private DateTime RunToContractEnd(Request request) + { + var utc = request.Block.Utc.AddMinutes(-1.0); + var tracker = new ChainRequestTracker(output, input.PurchaseId); + var ignoreLog = new NullLog(); + var chainState = new ChainState(ignoreLog, contracts, tracker, utc, false); + + while (!tracker.IsFinished) + { + utc += TimeSpan.FromHours(1.0); + if (utc > DateTime.UtcNow) + { + log.Log("Caught up to present moment without finding contract end."); + return DateTime.UtcNow; + } + + log.Log($"Querying up to {utc}"); + chainState.Update(utc); + } + + return tracker.FinishUtc; + } + + private ReserveSlotFunction[] Filter(ReserveSlotFunction[] calls) + { + return calls.Where(c => IsThisRequest(c.RequestId)).ToArray(); + } + + private Request? GetRequest() + { + var request = FindRequest(LastHour()); + if (request == null) request = FindRequest(LastDay()); + if (request == null) request = FindRequest(LastWeek()); + return request; + } + + private Request? FindRequest(TimeRange timeRange) + { + var events = contracts.GetEvents(timeRange); + var requests = events.GetStorageRequests(); + + foreach (var r in requests) + { + if (IsThisRequest(r.RequestId)) + { + return r; + } + } + + return null; + } + + private bool IsThisRequest(byte[] requestId) + { + return requestId.ToHex().ToLowerInvariant() == input.PurchaseId.ToLowerInvariant(); + } + + private static TimeRange LastHour() + { + return new TimeRange(DateTime.UtcNow.AddHours(-1.0), DateTime.UtcNow); + } + + private static TimeRange LastDay() + { + return new TimeRange(DateTime.UtcNow.AddDays(-1.0), DateTime.UtcNow); + } + + private static TimeRange LastWeek() + { + return new TimeRange(DateTime.UtcNow.AddDays(-7.0), DateTime.UtcNow); + } + } +} diff --git a/TraceContract/Config.cs b/TraceContract/Config.cs index 8f52e7cf..51c5e4d1 100644 --- a/TraceContract/Config.cs +++ b/TraceContract/Config.cs @@ -7,5 +7,41 @@ public string MarketplaceAddress { get; } = "0xDB2908d724a15d05c0B6B8e8441a8b36E67476d3"; public string TokenAddress { get; } = "0x34a22f3911De437307c6f4485931779670f78764"; public string Abi { get; } = @"[{""inputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":""configuration"",""type"":""tuple""},{""internalType"":""contract IERC20"",""name"":""token_"",""type"":""address""},{""internalType"":""contract IGroth16Verifier"",""name"":""verifier"",""type"":""address""}],""stateMutability"":""nonpayable"",""type"":""constructor""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""ProofSubmitted"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestCancelled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFailed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFulfilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFreed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""indexed"":false,""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""indexed"":false,""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""}],""name"":""StorageRequested"",""type"":""event""},{""inputs"":[],""name"":""config"",""outputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""fillSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""freeSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getActiveSlot"",""outputs"":[{""components"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""internalType"":""struct Marketplace.ActiveSlot"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getChallenge"",""outputs"":[{""internalType"":""bytes32"",""name"":"""",""type"":""bytes32""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getHost"",""outputs"":[{""internalType"":""address"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getPointer"",""outputs"":[{""internalType"":""uint8"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""getRequest"",""outputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""isProofRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""},{""internalType"":""Periods.Period"",""name"":""period"",""type"":""uint256""}],""name"":""markProofAsMissing"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""missingProofs"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""myRequests"",""outputs"":[{""internalType"":""RequestId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""mySlots"",""outputs"":[{""internalType"":""SlotId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestEnd"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestExpiry"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestState"",""outputs"":[{""internalType"":""enum RequestState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""}],""name"":""requestStorage"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""slotState"",""outputs"":[{""internalType"":""enum SlotState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""submitProof"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[],""name"":""token"",""outputs"":[{""internalType"":""contract IERC20"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""willProofBeRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""withdrawFunds"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""}]"; + + /// + /// Naming things is hard. + /// If the storage request is created at T=0, then fetching of the + /// storage node logs will begin from T=0 minus 'LogStartBeforeStorageContractStarts'. + /// + public TimeSpan LogStartBeforeStorageContractStarts { get; } = TimeSpan.FromMinutes(1.0); + + public string StorageNodesKubernetesNamespace = "codex"; + public string[] StorageNodesKubernetesContainerNames = [ + "codex-1-1", + //"codex-2-1", + //"codex-3-1", + //"codex-4-1", + //"codex-5-1", + //"codex-6-1", + //"codex-7-1", + //"codex-8-1", + //"codex-9-1", + "codex-10-1", + // "codex-validator-1-1", + ]; + + public Dictionary LogReplacements = new() + { + { "0xa1f988fBa23EFd5fA36F4c1a2D1E3c83e25bee4e", "codex 01" }, + { "0xa26a91310F9f2987AA7e0b1ca70e5C474c88ed34", "codex 02" }, + { "0x0CDC9d2D375300C46E13a679cD9eA5299A4FAc74", "codex 03" }, + { "0x7AF1a49A4a52e4bCe3789Ce3d43ff8AD8c8F2118", "codex 04" }, + { "0xfbbEB320c6c775f6565c7bcC732b2813Dd6E0cd3", "codex 05" }, + { "0x4A904CA0998B643eb42d4ae190a5821A4ac51E68", "codex 06" }, + { "0x2b8Ea47d0966B26DEec485c0fCcF0D1A8b52A0e8", "codex 07" }, + { "0x78F90A61d9a2aA93B61A7503Cc2177fFEF379021", "codex 08" }, + { "0xE7EEb996B3c817cEd03d10cd64A1325DA33D92e7", "codex 09" }, + { "0xD25C7609e97F40b66E74c0FcEbeA06D09423CC7e", "codex 10" } + }; } } diff --git a/TraceContract/ElasticSearchLogDownloader.cs b/TraceContract/ElasticSearchLogDownloader.cs new file mode 100644 index 00000000..8b40ed0b --- /dev/null +++ b/TraceContract/ElasticSearchLogDownloader.cs @@ -0,0 +1,242 @@ +using Core; +using Logging; +using Utils; +using WebUtils; + +namespace ContinuousTests +{ + public class ElasticSearchLogDownloader + { + private readonly ILog log; + private readonly IPluginTools tools; + private readonly string k8SNamespace; + + public ElasticSearchLogDownloader(ILog log, IPluginTools tools, string k8sNamespace) + { + this.log = log; + this.tools = tools; + k8SNamespace = k8sNamespace; + } + + public void Download(LogFile targetFile, string containerName, DateTime startUtc, DateTime endUtc) + { + try + { + DownloadLog(targetFile, containerName, startUtc, endUtc); + } + catch (Exception ex) + { + log.Error("Failed to download log: " + ex); + } + } + + private void DownloadLog(LogFile targetFile, string containerName, DateTime startUtc, DateTime endUtc) + { + log.Log($"Downloading log (from ElasticSearch) for container '{containerName}' within time range: " + + $"{startUtc.ToString("o")} - {endUtc.ToString("o")}"); + + var endpoint = CreateElasticSearchEndpoint(); + var queryTemplate = CreateQueryTemplate(containerName, startUtc, endUtc); + + targetFile.Write($"Downloading '{containerName}' to '{targetFile.Filename}'."); + var reconstructor = new LogReconstructor(targetFile, endpoint, queryTemplate); + reconstructor.DownloadFullLog(); + + log.Log("Log download finished."); + } + + private string CreateQueryTemplate(string containerName, DateTime startUtc, DateTime endUtc) + { + var start = startUtc.ToString("o"); + var end = endUtc.ToString("o"); + + //container_name : codex3-5 - deploymentName as stored in pod + // pod_namespace : codex - continuous - nolimits - tests - 1 + + //var source = "{ \"sort\": [ { \"@timestamp\": { \"order\": \"asc\" } } ], \"fields\": [ { \"field\": \"@timestamp\", \"format\": \"strict_date_optional_time\" }, { \"field\": \"pod_name\" }, { \"field\": \"message\" } ], \"size\": , \"_source\": false, \"query\": { \"bool\": { \"must\": [], \"filter\": [ { \"range\": { \"@timestamp\": { \"format\": \"strict_date_optional_time\", \"gte\": \"\", \"lte\": \"\" } } }, { \"match_phrase\": { \"pod_name\": \"\" } } ] } } }"; + var source = "{ \"sort\": [ { \"@timestamp\": { \"order\": \"asc\" } } ], \"fields\": [ { \"field\": \"@timestamp\", \"format\": \"strict_date_optional_time\" }, { \"field\": \"message\" } ], \"size\": , \"_source\": false, \"query\": { \"bool\": { \"must\": [], \"filter\": [ { \"range\": { \"@timestamp\": { \"format\": \"strict_date_optional_time\", \"gte\": \"\", \"lte\": \"\" } } }, { \"match_phrase\": { \"container_name\": \"\" } }, { \"match_phrase\": { \"pod_namespace\": \"\" } } ] } } }"; + return source + .Replace("", start) + .Replace("", end) + .Replace("", containerName) + .Replace("", k8SNamespace); + } + + private IEndpoint CreateElasticSearchEndpoint() + { + var serviceName = "elasticsearch"; + var k8sNamespace = "monitoring"; + var address = new Address("ElasticSearchEndpoint", $"http://{serviceName}.{k8sNamespace}.svc.cluster.local", 9200); + var baseUrl = ""; + + var http = tools.CreateHttp(address.ToString(), client => + { + client.DefaultRequestHeaders.Add("kbn-xsrf", "reporting"); + }); + + return http.CreateEndpoint(address, baseUrl); + } + + public class LogReconstructor + { + private readonly List queue = new List(); + private readonly LogFile targetFile; + private readonly IEndpoint endpoint; + private readonly string queryTemplate; + private const int sizeOfPage = 2000; + private string searchAfter = ""; + private int lastHits = 1; + private ulong? lastLogLine; + + public LogReconstructor(LogFile targetFile, IEndpoint endpoint, string queryTemplate) + { + this.targetFile = targetFile; + this.endpoint = endpoint; + this.queryTemplate = queryTemplate; + } + + public void DownloadFullLog() + { + while (lastHits > 0) + { + QueryElasticSearch(); + ProcessQueue(); + } + } + + private void QueryElasticSearch() + { + var query = queryTemplate + .Replace("", sizeOfPage.ToString()) + .Replace("", searchAfter); + + var response = endpoint.HttpPostString("_search", query); + + lastHits = response.hits.hits.Length; + if (lastHits > 0) + { + UpdateSearchAfter(response); + foreach (var hit in response.hits.hits) + { + AddHitToQueue(hit); + } + } + } + + private void AddHitToQueue(SearchHitEntry hit) + { + var message = hit.fields.message.Single(); + var number = ParseCountNumber(message); + if (number != null) + { + queue.Add(new LogQueueEntry(message, number.Value)); + } + } + + private ulong? ParseCountNumber(string message) + { + if (string.IsNullOrEmpty(message)) return null; + var tokens = message.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (!tokens.Any()) return null; + var countToken = tokens.SingleOrDefault(t => t.StartsWith("count=")); + if (countToken == null) return null; + var number = countToken.Substring(6); + if (ulong.TryParse(number, out ulong value)) + { + return value; + } + return null; + } + + private void UpdateSearchAfter(SearchResponse response) + { + var uniqueSearchNumbers = response.hits.hits.Select(h => h.sort.Single()).Distinct().ToList(); + uniqueSearchNumbers.Reverse(); + + var searchNumber = GetSearchNumber(uniqueSearchNumbers); + searchAfter = $"\"search_after\": [{searchNumber}],"; + } + + private long GetSearchNumber(List uniqueSearchNumbers) + { + if (uniqueSearchNumbers.Count == 1) return uniqueSearchNumbers.First(); + return uniqueSearchNumbers.Skip(1).First(); + } + + private void ProcessQueue() + { + if (lastLogLine == null) + { + lastLogLine = queue.Min(q => q.Number) - 1; + } + + while (queue.Any()) + { + ulong wantedNumber = lastLogLine.Value + 1; + + DeleteOldEntries(wantedNumber); + + var currentEntry = queue.FirstOrDefault(e => e.Number == wantedNumber); + + if (currentEntry != null) + { + WriteEntryToFile(currentEntry); + queue.Remove(currentEntry); + lastLogLine = currentEntry.Number; + } + else + { + // The line number we want is not in the queue. + // It will be returned by the elastic search query, some time in the future. + // Stop processing the queue for now. + return; + } + } + } + + private void WriteEntryToFile(LogQueueEntry currentEntry) + { + targetFile.Write(currentEntry.Message); + } + + private void DeleteOldEntries(ulong wantedNumber) + { + queue.RemoveAll(e => e.Number < wantedNumber); + } + + public class LogQueueEntry + { + public LogQueueEntry(string message, ulong number) + { + Message = message; + Number = number; + } + + public string Message { get; } + public ulong Number { get; } + } + + public class SearchResponse + { + public SearchHits hits { get; set; } = new SearchHits(); + } + + public class SearchHits + { + public SearchHitEntry[] hits { get; set; } = Array.Empty(); + } + + public class SearchHitEntry + { + public SearchHitFields fields { get; set; } = new SearchHitFields(); + public long[] sort { get; set; } = Array.Empty(); + } + + public class SearchHitFields + { + public string[] @timestamp { get; set; } = Array.Empty(); + public string[] message { get; set; } = Array.Empty(); + } + } + } +} diff --git a/TraceContract/Input.cs b/TraceContract/Input.cs new file mode 100644 index 00000000..50a23ebb --- /dev/null +++ b/TraceContract/Input.cs @@ -0,0 +1,13 @@ +namespace TraceContract +{ + public class Input + { + public string PurchaseId { get; } = + // expired: + "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + + // started: + //"066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; + } + +} diff --git a/TraceContract/Output.cs b/TraceContract/Output.cs index 6a731e0f..d7c230ba 100644 --- a/TraceContract/Output.cs +++ b/TraceContract/Output.cs @@ -8,61 +8,112 @@ namespace TraceContract { public class Output { - private readonly ILog log; - - public Output(ILog log) + private class Entry { - this.log = log; + public Entry(DateTime utc, string msg) + { + Utc = utc; + Msg = msg; + } + + public DateTime Utc { get; } + public string Msg { get; } + } + + private readonly ILog log; + private readonly List entries = new(); + private readonly string folder; + private readonly List files = new(); + + public Output(ILog log, Input input, Config config) + { + folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(folder); + + var filename = Path.Combine(folder, $"Contract_{input.PurchaseId}"); + var fileLog = new FileLog(filename); + files.Add(fileLog.FullFilename); + foreach (var pair in config.LogReplacements) + { + fileLog.AddStringReplace(pair.Key, pair.Value); + fileLog.AddStringReplace(pair.Key.ToLowerInvariant(), pair.Value); + } + + log.Log($"Logging to '{filename}'"); + this.log = new LogSplitter(fileLog, log); } public void LogRequestCreated(RequestEvent requestEvent) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, $"Storage request created: '{requestEvent.Request.Request.Id}'"); } public void LogRequestCancelled(RequestEvent requestEvent) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, "Expired"); } public void LogRequestFailed(RequestEvent requestEvent) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, "Failed"); } public void LogRequestFinished(RequestEvent requestEvent) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, "Finished"); } public void LogRequestStarted(RequestEvent requestEvent) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, "Started"); } public void LogSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, $"Slot filled. Index: {slotIndex} Host: '{host}'"); } public void LogSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, $"Slot freed. Index: {slotIndex}"); } public void LogSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) { - throw new NotImplementedException(); + Add(requestEvent.Block.Utc, $"Slot reservations full. Index: {slotIndex}"); } public void LogReserveSlotCalls(ReserveSlotFunction[] reserveSlotFunctions) { - throw new NotImplementedException(); + foreach (var call in reserveSlotFunctions) LogReserveSlotCall(call); } public void WriteContractEvents() { - throw new NotImplementedException(); + var sorted = entries.OrderBy(e => e.Utc).ToArray(); + foreach (var e in sorted) Write(e); + } + + private void Write(Entry e) + { + log.Log($"[{Time.FormatTimestamp(e.Utc)}] {e.Msg}"); + } + + private void LogReserveSlotCall(ReserveSlotFunction call) + { + Add(call.Block.Utc, $"Reserve-slot called. Index: {call.SlotIndex} Host: '{call.FromAddress}'"); + } + + private void Add(DateTime utc, string msg) + { + entries.Add(new Entry(utc, msg)); + } + + public LogFile CreateNodeLogTargetFile(string node) + { + var file = log.CreateSubfile(node); + files.Add(file.Filename); + return file; } } } diff --git a/TraceContract/Program.cs b/TraceContract/Program.cs index eabb7e16..0b7f3911 100644 --- a/TraceContract/Program.cs +++ b/TraceContract/Program.cs @@ -1,30 +1,21 @@ -using System.Numerics; -using BlockchainUtils; +using BlockchainUtils; using CodexContractsPlugin; -using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; +using ContinuousTests; using Core; using GethPlugin; using Logging; -using Nethereum.Hex.HexConvertors.Extensions; using Utils; namespace TraceContract { - public class Input - { - public string PurchaseId { get; } = - // expired: - //"a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; - - // started: - "066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; - } - public class Program { public static void Main(string[] args) { + ProjectPlugin.Load(); + ProjectPlugin.Load(); + var p = new Program(); p.Run(); } @@ -36,7 +27,7 @@ namespace TraceContract public Program() { - output = new(log); + output = new(log, input, config); } private void Run() @@ -54,29 +45,32 @@ namespace TraceContract private void TracePurchase() { Log("Setting up..."); - var contracts = ConnectCodexContracts(); - - var chainTracer = new ChainTracer(log, contracts, input, output); - var requestTimeRange = chainTracer.TraceChainTimeline(); - - Log("Done"); - } - - private ICodexContracts ConnectCodexContracts() - { - ProjectPlugin.Load(); - ProjectPlugin.Load(); - var entryPoint = new EntryPoint(log, new KubernetesWorkflow.Configuration(null, TimeSpan.FromMinutes(1.0), TimeSpan.FromSeconds(10.0), "_Unused!_"), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); entryPoint.Announce(); var ci = entryPoint.CreateInterface(); + var contracts = ConnectCodexContracts(ci); + + var chainTracer = new ChainTracer(log, contracts, input, output); + var requestTimeRange = chainTracer.TraceChainTimeline(); + + Log("Downloading storage nodes logs for the request timerange..."); + DownloadStorageNodeLogs(requestTimeRange, entryPoint.Tools); + + // package everything + + entryPoint.Decommission(false, false, false); + Log("Done"); + } + + private ICodexContracts ConnectCodexContracts(CoreInterface ci) + { var account = EthAccountGenerator.GenerateNew(); var blockCache = new BlockCache(); var geth = new CustomGethNode(log, blockCache, config.RpcEndpoint, config.GethPort, account.PrivateKey); var deployment = new CodexContractsDeployment( - config: new CodexContractsPlugin.Marketplace.MarketplaceConfig(), + config: new MarketplaceConfig(), marketplaceAddress: config.MarketplaceAddress, abi: config.Abi, tokenAddress: config.TokenAddress @@ -84,197 +78,23 @@ namespace TraceContract return ci.WrapCodexContractsDeployment(geth, deployment); } + private void DownloadStorageNodeLogs(TimeRange requestTimeRange, IPluginTools tools) + { + var start = requestTimeRange.From - config.LogStartBeforeStorageContractStarts; + + foreach (var node in config.StorageNodesKubernetesContainerNames) + { + Log($"Downloading logs from '{node}'..."); + + var targetFile = output.CreateNodeLogTargetFile(node); + var downloader = new ElasticSearchLogDownloader(log, tools, config.StorageNodesKubernetesNamespace); + downloader.Download(targetFile, node, start, requestTimeRange.To); + } + } + private void Log(string msg) { log.Log(msg); } } - - public class ChainTracer - { - private readonly ILog log; - private readonly ICodexContracts contracts; - private readonly Input input; - private readonly Output output; - - public ChainTracer(ILog log, ICodexContracts contracts, Input input, Output output) - { - this.log = log; - this.contracts = contracts; - this.input = input; - this.output = output; - } - - public TimeRange TraceChainTimeline() - { - var request = GetRequest(); - if (request == null) throw new Exception("Failed to find the purchase in the last week of transactions."); - - var utc = request.Block.Utc.AddMinutes(-1.0); - var tracker = new ChainRequestTracker(output, input.PurchaseId); - var ignoreLog = new NullLog(); - var chainState = new ChainState(ignoreLog, contracts, tracker, utc, false); - - while (!tracker.IsFinished) - { - utc += TimeSpan.FromHours(1.0); - chainState.Update(utc); - } - - var requestTimeline = new TimeRange(request.Block.Utc.AddMinutes(-1.0), tracker.FinishUtc.AddMinutes(1.0)); - - // For this timeline, we log all the calls to reserve-slot. - var events = contracts.GetEvents(requestTimeline); - output.LogReserveSlotCalls(Filter(events.GetReserveSlotCalls())); - - output.WriteContractEvents(); - - return requestTimeline; - } - - private ReserveSlotFunction[] Filter(ReserveSlotFunction[] calls) - { - return calls.Where(c => IsThisRequest(c.RequestId)).ToArray(); - } - - private Request? GetRequest() - { - var request = FindRequest(LastHour()); - if (request == null) request = FindRequest(LastDay()); - if (request == null) request = FindRequest(LastWeek()); - return request; - } - - private Request? FindRequest(TimeRange timeRange) - { - var events = contracts.GetEvents(timeRange); - var requests = events.GetStorageRequests(); - - foreach (var r in requests) - { - if (IsThisRequest(r.RequestId)) - { - return r; - } - } - - return null; - } - - private bool IsThisRequest(byte[] requestId) - { - return requestId.ToHex().ToLowerInvariant() == input.PurchaseId.ToLowerInvariant(); - } - - private static TimeRange LastHour() - { - return new TimeRange(DateTime.UtcNow.AddHours(-1.0), DateTime.UtcNow); - } - - private static TimeRange LastDay() - { - return new TimeRange(DateTime.UtcNow.AddDays(-1.0), DateTime.UtcNow); - } - - private static TimeRange LastWeek() - { - return new TimeRange(DateTime.UtcNow.AddDays(-7.0), DateTime.UtcNow); - } - } - - public class ChainRequestTracker : IChainStateChangeHandler - { - private readonly string requestId; - private readonly Output output; - - public ChainRequestTracker(Output output, string requestId) - { - this.requestId = requestId.ToLowerInvariant(); - this.output = output; - } - - public bool IsFinished { get; private set; } = false; - public DateTime FinishUtc { get; private set; } = DateTime.MinValue; - - public void OnError(string msg) - { - } - - public void OnNewRequest(RequestEvent requestEvent) - { - if (IsMyRequest(requestEvent)) output.LogRequestCreated(requestEvent); - } - - public void OnProofSubmitted(BlockTimeEntry block, string id) - { - } - - public void OnRequestCancelled(RequestEvent requestEvent) - { - if (IsMyRequest(requestEvent)) - { - IsFinished = true; - FinishUtc = requestEvent.Block.Utc; - output.LogRequestCancelled(requestEvent); - } - } - - public void OnRequestFailed(RequestEvent requestEvent) - { - if (IsMyRequest(requestEvent)) - { - IsFinished = true; - FinishUtc = requestEvent.Block.Utc; - output.LogRequestFailed(requestEvent); - } - } - - public void OnRequestFinished(RequestEvent requestEvent) - { - if (IsMyRequest(requestEvent)) - { - IsFinished = true; - FinishUtc = requestEvent.Block.Utc; - output.LogRequestFinished(requestEvent); - } - } - - public void OnRequestFulfilled(RequestEvent requestEvent) - { - if (IsMyRequest(requestEvent)) - { - output.LogRequestStarted(requestEvent); - } - } - - public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) - { - if (IsMyRequest(requestEvent)) - { - output.LogSlotFilled(requestEvent, host, slotIndex); - } - } - - public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) - { - if (IsMyRequest(requestEvent)) - { - output.LogSlotFreed(requestEvent, slotIndex); - } - } - - public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) - { - if (IsMyRequest(requestEvent)) - { - output.LogSlotReservationsFull(requestEvent, slotIndex); - } - } - - private bool IsMyRequest(RequestEvent requestEvent) - { - return requestId == requestEvent.Request.Request.Id.ToLowerInvariant(); - } - } - } From 74c44c4ef87bfd20558b01431b41632212814b14 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 14:41:33 +0200 Subject: [PATCH 40/69] wip setting up for packing and upload --- TraceContract/Config.cs | 34 ++++++++++++++++----- TraceContract/ElasticSearchLogDownloader.cs | 29 ++++++++++++------ TraceContract/Input.cs | 20 ++++++++---- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/TraceContract/Config.cs b/TraceContract/Config.cs index 51c5e4d1..189beb5e 100644 --- a/TraceContract/Config.cs +++ b/TraceContract/Config.cs @@ -15,17 +15,18 @@ /// public TimeSpan LogStartBeforeStorageContractStarts { get; } = TimeSpan.FromMinutes(1.0); + public string ElasticSearchUrl { get; } = $"https://es.testnet.codex.storage"; public string StorageNodesKubernetesNamespace = "codex"; public string[] StorageNodesKubernetesContainerNames = [ "codex-1-1", - //"codex-2-1", - //"codex-3-1", - //"codex-4-1", - //"codex-5-1", - //"codex-6-1", - //"codex-7-1", - //"codex-8-1", - //"codex-9-1", + "codex-2-1", + "codex-3-1", + "codex-4-1", + "codex-5-1", + "codex-6-1", + "codex-7-1", + "codex-8-1", + "codex-9-1", "codex-10-1", // "codex-validator-1-1", ]; @@ -43,5 +44,22 @@ { "0xE7EEb996B3c817cEd03d10cd64A1325DA33D92e7", "codex 09" }, { "0xD25C7609e97F40b66E74c0FcEbeA06D09423CC7e", "codex 10" } }; + + public string GetElasticSearchUsername() + { + return GetEnvVar("ES_USERNAME", "username"); + } + + public string GetElasticSearchPassword() + { + return GetEnvVar("ES_PASSWORD", "password"); + } + + private string GetEnvVar(string name, string defaultValue) + { + var v = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(v)) return defaultValue; + return v; + } } } diff --git a/TraceContract/ElasticSearchLogDownloader.cs b/TraceContract/ElasticSearchLogDownloader.cs index 8b40ed0b..f1a76557 100644 --- a/TraceContract/ElasticSearchLogDownloader.cs +++ b/TraceContract/ElasticSearchLogDownloader.cs @@ -1,21 +1,22 @@ -using Core; +using System.Text; +using Core; using Logging; using Utils; using WebUtils; -namespace ContinuousTests +namespace TraceContract { public class ElasticSearchLogDownloader { private readonly ILog log; private readonly IPluginTools tools; - private readonly string k8SNamespace; + private readonly Config config; - public ElasticSearchLogDownloader(ILog log, IPluginTools tools, string k8sNamespace) + public ElasticSearchLogDownloader(ILog log, IPluginTools tools, Config config) { this.log = log; this.tools = tools; - k8SNamespace = k8sNamespace; + this.config = config; } public void Download(LogFile targetFile, string containerName, DateTime startUtc, DateTime endUtc) @@ -59,19 +60,29 @@ namespace ContinuousTests .Replace("", start) .Replace("", end) .Replace("", containerName) - .Replace("", k8SNamespace); + .Replace("", config.StorageNodesKubernetesNamespace); } private IEndpoint CreateElasticSearchEndpoint() { - var serviceName = "elasticsearch"; - var k8sNamespace = "monitoring"; - var address = new Address("ElasticSearchEndpoint", $"http://{serviceName}.{k8sNamespace}.svc.cluster.local", 9200); + //var serviceName = "elasticsearch"; + //var k8sNamespace = "monitoring"; + //var address = new Address("ElasticSearchEndpoint", $"http://{serviceName}.{k8sNamespace}.svc.cluster.local", 9200); + + var address = new Address("TestnetElasticSearchEndpoint", config.ElasticSearchUrl, 443); var baseUrl = ""; + var username = config.GetElasticSearchUsername(); + var password = config.GetElasticSearchPassword(); + + var base64Creds = Convert.ToBase64String( + Encoding.ASCII.GetBytes($"{username}:{password}") + ); + var http = tools.CreateHttp(address.ToString(), client => { client.DefaultRequestHeaders.Add("kbn-xsrf", "reporting"); + client.DefaultRequestHeaders.Add("Authorization", "Basic " + base64Creds); }); return http.CreateEndpoint(address, baseUrl); diff --git a/TraceContract/Input.cs b/TraceContract/Input.cs index 50a23ebb..a0c63a03 100644 --- a/TraceContract/Input.cs +++ b/TraceContract/Input.cs @@ -2,12 +2,20 @@ { public class Input { - public string PurchaseId { get; } = - // expired: - "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + public string PurchaseId + { + get + { + var v = Environment.GetEnvironmentVariable("PURCHASE_ID"); + if (!string.IsNullOrEmpty(v)) return v; - // started: - //"066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; + return + // expired: + "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + + // started: + //"066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; + } + } } - } From 873e4750ab4ec6f3188db32aa89a786b93dfe591 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 15:09:38 +0200 Subject: [PATCH 41/69] packaging --- TraceContract/Config.cs | 5 +++++ TraceContract/Output.cs | 35 +++++++++++++++++++++++++---------- TraceContract/Program.cs | 10 ++++++---- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/TraceContract/Config.cs b/TraceContract/Config.cs index 189beb5e..f6b7f1ad 100644 --- a/TraceContract/Config.cs +++ b/TraceContract/Config.cs @@ -55,6 +55,11 @@ return GetEnvVar("ES_PASSWORD", "password"); } + public string GetOuputFolder() + { + return GetEnvVar("OUTPUT_FOLDER", "/tmp"); + } + private string GetEnvVar(string name, string defaultValue) { var v = Environment.GetEnvironmentVariable(name); diff --git a/TraceContract/Output.cs b/TraceContract/Output.cs index d7c230ba..a5d09bb1 100644 --- a/TraceContract/Output.cs +++ b/TraceContract/Output.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.IO.Compression; +using System.Numerics; using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; using Logging; @@ -24,15 +25,17 @@ namespace TraceContract private readonly List entries = new(); private readonly string folder; private readonly List files = new(); + private readonly Input input; + private readonly Config config; public Output(ILog log, Input input, Config config) { folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(folder); - var filename = Path.Combine(folder, $"Contract_{input.PurchaseId}"); + var filename = Path.Combine(folder, $"contract_{input.PurchaseId}"); var fileLog = new FileLog(filename); - files.Add(fileLog.FullFilename); + files.Add(fileLog.FullFilename + ".log"); foreach (var pair in config.LogReplacements) { fileLog.AddStringReplace(pair.Key, pair.Value); @@ -41,6 +44,8 @@ namespace TraceContract log.Log($"Logging to '{filename}'"); this.log = new LogSplitter(fileLog, log); + this.input = input; + this.config = config; } public void LogRequestCreated(RequestEvent requestEvent) @@ -94,6 +99,13 @@ namespace TraceContract foreach (var e in sorted) Write(e); } + public LogFile CreateNodeLogTargetFile(string node) + { + var file = log.CreateSubfile(node); + files.Add(file.Filename); + return file; + } + private void Write(Entry e) { log.Log($"[{Time.FormatTimestamp(e.Utc)}] {e.Msg}"); @@ -104,16 +116,19 @@ namespace TraceContract Add(call.Block.Utc, $"Reserve-slot called. Index: {call.SlotIndex} Host: '{call.FromAddress}'"); } + public string Package() + { + var outputFolder = config.GetOuputFolder(); + Directory.CreateDirectory(outputFolder); + var filename = Path.Combine(outputFolder, $"contract_{input.PurchaseId}.zip"); + + ZipFile.CreateFromDirectory(folder, filename); + return filename; + } + private void Add(DateTime utc, string msg) { entries.Add(new Entry(utc, msg)); } - - public LogFile CreateNodeLogTargetFile(string node) - { - var file = log.CreateSubfile(node); - files.Add(file.Filename); - return file; - } } } diff --git a/TraceContract/Program.cs b/TraceContract/Program.cs index 0b7f3911..67189161 100644 --- a/TraceContract/Program.cs +++ b/TraceContract/Program.cs @@ -1,7 +1,6 @@ using BlockchainUtils; using CodexContractsPlugin; using CodexContractsPlugin.Marketplace; -using ContinuousTests; using Core; using GethPlugin; using Logging; @@ -56,7 +55,9 @@ namespace TraceContract Log("Downloading storage nodes logs for the request timerange..."); DownloadStorageNodeLogs(requestTimeRange, entryPoint.Tools); - // package everything + Log("Packaging..."); + var zipFilename = output.Package(); + Log($"Saved to '{zipFilename}'"); entryPoint.Decommission(false, false, false); Log("Done"); @@ -87,8 +88,9 @@ namespace TraceContract Log($"Downloading logs from '{node}'..."); var targetFile = output.CreateNodeLogTargetFile(node); - var downloader = new ElasticSearchLogDownloader(log, tools, config.StorageNodesKubernetesNamespace); - downloader.Download(targetFile, node, start, requestTimeRange.To); + targetFile.Write("TODO!"); + //var downloader = new ElasticSearchLogDownloader(log, tools, config); + //downloader.Download(targetFile, node, start, requestTimeRange.To); } } From 2ca2a6149cfd003c4ffccad489e7d97ad459f428 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 15:22:08 +0200 Subject: [PATCH 42/69] Setting up github action --- .github/workflows/trace-contract.yaml | 42 +++++++++++++++++++ .../TraceContract}/ChainRequestTracker.cs | 0 .../TraceContract}/ChainTracer.cs | 0 .../TraceContract}/Config.cs | 0 .../ElasticSearchLogDownloader.cs | 0 .../TraceContract}/Input.cs | 0 .../TraceContract}/Output.cs | 0 .../TraceContract}/Program.cs | 0 .../TraceContract}/TraceContract.csproj | 4 +- cs-codex-dist-testing.sln | 12 +++--- 10 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/trace-contract.yaml rename {TraceContract => Tools/TraceContract}/ChainRequestTracker.cs (100%) rename {TraceContract => Tools/TraceContract}/ChainTracer.cs (100%) rename {TraceContract => Tools/TraceContract}/Config.cs (100%) rename {TraceContract => Tools/TraceContract}/ElasticSearchLogDownloader.cs (100%) rename {TraceContract => Tools/TraceContract}/Input.cs (100%) rename {TraceContract => Tools/TraceContract}/Output.cs (100%) rename {TraceContract => Tools/TraceContract}/Program.cs (100%) rename {TraceContract => Tools/TraceContract}/TraceContract.csproj (51%) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml new file mode 100644 index 00000000..b24451ef --- /dev/null +++ b/.github/workflows/trace-contract.yaml @@ -0,0 +1,42 @@ +name: Trace Contract + +on: + workflow_dispatch: + inputs: + purchaseid: + description: "Testnet Purchase ID" + required: true + type: string + +env: + SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }} + BRANCH: ${{ github.ref_name }} + OUTPUT_FOLDER: "/tmp" + ES_USERNAME: ${{ secrets.ES_USERNAME }} + ES_PASSWORD: ${{ secrets.ES_PASSWORD }} + +jobs: + run_tests: + name: Run Release Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ inputs.workflow_source }} + + - name: Variables + run: | + echo "PURCHASE_ID=${{ inputs.purchaseid }}" >> $GITHUB_ENV + + - name: Run Trace + run: | + dotnet run --project Tools/TraceContract + + - name: Upload output + uses: actions/upload-artifact@v4 + with: + name: contract-trace + path: /tmp/* + if-no-files-found: error + retention-days: 7 diff --git a/TraceContract/ChainRequestTracker.cs b/Tools/TraceContract/ChainRequestTracker.cs similarity index 100% rename from TraceContract/ChainRequestTracker.cs rename to Tools/TraceContract/ChainRequestTracker.cs diff --git a/TraceContract/ChainTracer.cs b/Tools/TraceContract/ChainTracer.cs similarity index 100% rename from TraceContract/ChainTracer.cs rename to Tools/TraceContract/ChainTracer.cs diff --git a/TraceContract/Config.cs b/Tools/TraceContract/Config.cs similarity index 100% rename from TraceContract/Config.cs rename to Tools/TraceContract/Config.cs diff --git a/TraceContract/ElasticSearchLogDownloader.cs b/Tools/TraceContract/ElasticSearchLogDownloader.cs similarity index 100% rename from TraceContract/ElasticSearchLogDownloader.cs rename to Tools/TraceContract/ElasticSearchLogDownloader.cs diff --git a/TraceContract/Input.cs b/Tools/TraceContract/Input.cs similarity index 100% rename from TraceContract/Input.cs rename to Tools/TraceContract/Input.cs diff --git a/TraceContract/Output.cs b/Tools/TraceContract/Output.cs similarity index 100% rename from TraceContract/Output.cs rename to Tools/TraceContract/Output.cs diff --git a/TraceContract/Program.cs b/Tools/TraceContract/Program.cs similarity index 100% rename from TraceContract/Program.cs rename to Tools/TraceContract/Program.cs diff --git a/TraceContract/TraceContract.csproj b/Tools/TraceContract/TraceContract.csproj similarity index 51% rename from TraceContract/TraceContract.csproj rename to Tools/TraceContract/TraceContract.csproj index 3bcdeedb..51fd39c6 100644 --- a/TraceContract/TraceContract.csproj +++ b/Tools/TraceContract/TraceContract.csproj @@ -8,9 +8,7 @@ - - - + diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index 18dd9a2e..a10e32d6 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -86,7 +86,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodexClient", "ProjectPlugi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebUtils", "Framework\WebUtils\WebUtils.csproj", "{372C9E5D-5453-4D45-9948-E9324E21AD65}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceContract", "TraceContract\TraceContract.csproj", "{6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceContract", "Tools\TraceContract\TraceContract.csproj", "{58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -234,10 +234,10 @@ Global {372C9E5D-5453-4D45-9948-E9324E21AD65}.Debug|Any CPU.Build.0 = Debug|Any CPU {372C9E5D-5453-4D45-9948-E9324E21AD65}.Release|Any CPU.ActiveCfg = Release|Any CPU {372C9E5D-5453-4D45-9948-E9324E21AD65}.Release|Any CPU.Build.0 = Release|Any CPU - {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC}.Release|Any CPU.Build.0 = Release|Any CPU + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -278,7 +278,7 @@ Global {4648B5AA-A0A7-44BA-89BC-2FD57370943C} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {9AF12703-29AF-416D-9781-204223D6D0E5} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124} {372C9E5D-5453-4D45-9948-E9324E21AD65} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} - {6F4C72D7-4B6E-45FD-93C6-2099CBE5B2AC} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C} From 9fec8b341b3c449c312e4e947be3e2630bc0894a Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 15:42:03 +0200 Subject: [PATCH 43/69] fixes elastic-search query --- Tools/TraceContract/Config.cs | 3 +-- .../ElasticSearchLogDownloader.cs | 22 +++++++++---------- Tools/TraceContract/Program.cs | 8 +++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs index f6b7f1ad..f52a0053 100644 --- a/Tools/TraceContract/Config.cs +++ b/Tools/TraceContract/Config.cs @@ -16,8 +16,7 @@ public TimeSpan LogStartBeforeStorageContractStarts { get; } = TimeSpan.FromMinutes(1.0); public string ElasticSearchUrl { get; } = $"https://es.testnet.codex.storage"; - public string StorageNodesKubernetesNamespace = "codex"; - public string[] StorageNodesKubernetesContainerNames = [ + public string[] StorageNodesKubernetesPodNames = [ "codex-1-1", "codex-2-1", "codex-3-1", diff --git a/Tools/TraceContract/ElasticSearchLogDownloader.cs b/Tools/TraceContract/ElasticSearchLogDownloader.cs index f1a76557..a55d85ca 100644 --- a/Tools/TraceContract/ElasticSearchLogDownloader.cs +++ b/Tools/TraceContract/ElasticSearchLogDownloader.cs @@ -19,11 +19,11 @@ namespace TraceContract this.config = config; } - public void Download(LogFile targetFile, string containerName, DateTime startUtc, DateTime endUtc) + public void Download(LogFile targetFile, string podName, DateTime startUtc, DateTime endUtc) { try { - DownloadLog(targetFile, containerName, startUtc, endUtc); + DownloadLog(targetFile, podName, startUtc, endUtc); } catch (Exception ex) { @@ -31,22 +31,22 @@ namespace TraceContract } } - private void DownloadLog(LogFile targetFile, string containerName, DateTime startUtc, DateTime endUtc) + private void DownloadLog(LogFile targetFile, string podName, DateTime startUtc, DateTime endUtc) { - log.Log($"Downloading log (from ElasticSearch) for container '{containerName}' within time range: " + + log.Log($"Downloading log (from ElasticSearch) for pod '{podName}' within time range: " + $"{startUtc.ToString("o")} - {endUtc.ToString("o")}"); var endpoint = CreateElasticSearchEndpoint(); - var queryTemplate = CreateQueryTemplate(containerName, startUtc, endUtc); + var queryTemplate = CreateQueryTemplate(podName, startUtc, endUtc); - targetFile.Write($"Downloading '{containerName}' to '{targetFile.Filename}'."); + targetFile.Write($"Downloading '{podName}' to '{targetFile.Filename}'."); var reconstructor = new LogReconstructor(targetFile, endpoint, queryTemplate); reconstructor.DownloadFullLog(); log.Log("Log download finished."); } - private string CreateQueryTemplate(string containerName, DateTime startUtc, DateTime endUtc) + private string CreateQueryTemplate(string podName, DateTime startUtc, DateTime endUtc) { var start = startUtc.ToString("o"); var end = endUtc.ToString("o"); @@ -55,12 +55,12 @@ namespace TraceContract // pod_namespace : codex - continuous - nolimits - tests - 1 //var source = "{ \"sort\": [ { \"@timestamp\": { \"order\": \"asc\" } } ], \"fields\": [ { \"field\": \"@timestamp\", \"format\": \"strict_date_optional_time\" }, { \"field\": \"pod_name\" }, { \"field\": \"message\" } ], \"size\": , \"_source\": false, \"query\": { \"bool\": { \"must\": [], \"filter\": [ { \"range\": { \"@timestamp\": { \"format\": \"strict_date_optional_time\", \"gte\": \"\", \"lte\": \"\" } } }, { \"match_phrase\": { \"pod_name\": \"\" } } ] } } }"; - var source = "{ \"sort\": [ { \"@timestamp\": { \"order\": \"asc\" } } ], \"fields\": [ { \"field\": \"@timestamp\", \"format\": \"strict_date_optional_time\" }, { \"field\": \"message\" } ], \"size\": , \"_source\": false, \"query\": { \"bool\": { \"must\": [], \"filter\": [ { \"range\": { \"@timestamp\": { \"format\": \"strict_date_optional_time\", \"gte\": \"\", \"lte\": \"\" } } }, { \"match_phrase\": { \"container_name\": \"\" } }, { \"match_phrase\": { \"pod_namespace\": \"\" } } ] } } }"; + var source = "{ \"sort\": [ { \"@timestamp\": { \"order\": \"asc\" } } ], \"fields\": [ { \"field\": \"@timestamp\", \"format\": \"strict_date_optional_time\" }, { \"field\": \"message\" } ], \"size\": , \"_source\": false, \"query\": { \"bool\": { \"must\": [], \"filter\": [ { \"range\": { \"@timestamp\": { \"format\": \"strict_date_optional_time\", \"gte\": \"\", \"lte\": \"\" } } }, { \"match_phrase\": { \"pod_name\": \"\" } } ] } } }"; return source .Replace("", start) .Replace("", end) - .Replace("", containerName) - .Replace("", config.StorageNodesKubernetesNamespace); + .Replace("", podName); + //.Replace("", config.StorageNodesKubernetesNamespace); } private IEndpoint CreateElasticSearchEndpoint() @@ -121,7 +121,7 @@ namespace TraceContract .Replace("", sizeOfPage.ToString()) .Replace("", searchAfter); - var response = endpoint.HttpPostString("_search", query); + var response = endpoint.HttpPostString("/_search", query); lastHits = response.hits.hits.Length; if (lastHits > 0) diff --git a/Tools/TraceContract/Program.cs b/Tools/TraceContract/Program.cs index 67189161..d239373c 100644 --- a/Tools/TraceContract/Program.cs +++ b/Tools/TraceContract/Program.cs @@ -65,7 +65,6 @@ namespace TraceContract private ICodexContracts ConnectCodexContracts(CoreInterface ci) { - var account = EthAccountGenerator.GenerateNew(); var blockCache = new BlockCache(); var geth = new CustomGethNode(log, blockCache, config.RpcEndpoint, config.GethPort, account.PrivateKey); @@ -83,14 +82,13 @@ namespace TraceContract { var start = requestTimeRange.From - config.LogStartBeforeStorageContractStarts; - foreach (var node in config.StorageNodesKubernetesContainerNames) + foreach (var node in config.StorageNodesKubernetesPodNames) { Log($"Downloading logs from '{node}'..."); var targetFile = output.CreateNodeLogTargetFile(node); - targetFile.Write("TODO!"); - //var downloader = new ElasticSearchLogDownloader(log, tools, config); - //downloader.Download(targetFile, node, start, requestTimeRange.To); + var downloader = new ElasticSearchLogDownloader(log, tools, config); + downloader.Download(targetFile, node, start, requestTimeRange.To); } } From 0add69c42e871d5da15c1acc0e415677bbfa36e9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 15:56:06 +0200 Subject: [PATCH 44/69] pulls ES host from env vars --- Tools/TraceContract/Config.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs index f52a0053..5cdce6e7 100644 --- a/Tools/TraceContract/Config.cs +++ b/Tools/TraceContract/Config.cs @@ -15,7 +15,14 @@ /// public TimeSpan LogStartBeforeStorageContractStarts { get; } = TimeSpan.FromMinutes(1.0); - public string ElasticSearchUrl { get; } = $"https://es.testnet.codex.storage"; + public string ElasticSearchUrl + { + get + { + return GetEnvVar("ES_HOST", "es_host"); + } + } + public string[] StorageNodesKubernetesPodNames = [ "codex-1-1", "codex-2-1", From 6e610f847008cd897917374ed0a975bc24a72af6 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 20 May 2025 16:01:28 +0200 Subject: [PATCH 45/69] sets codex image to 0.2.2 --- ProjectPlugins/CodexPlugin/CodexDockerImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index 55301cf8..787727e5 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,7 +2,7 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.1-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.2-dist-tests"; public static string Override { get; set; } = string.Empty; From 61202a46b2f6e31a0e1d8441061069b1065d286b Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 21 May 2025 07:51:11 +0200 Subject: [PATCH 46/69] adds ES_HOST env var --- .github/workflows/trace-contract.yaml | 1 + Framework/WebUtils/Endpoint.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index b24451ef..a86202f4 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -14,6 +14,7 @@ env: OUTPUT_FOLDER: "/tmp" ES_USERNAME: ${{ secrets.ES_USERNAME }} ES_PASSWORD: ${{ secrets.ES_PASSWORD }} + ES_HOST: ${{ secrets.ES_HOST }} jobs: run_tests: diff --git a/Framework/WebUtils/Endpoint.cs b/Framework/WebUtils/Endpoint.cs index bf1a95a1..5f727c8d 100644 --- a/Framework/WebUtils/Endpoint.cs +++ b/Framework/WebUtils/Endpoint.cs @@ -85,7 +85,7 @@ namespace WebUtils var result = Deserialize(response); if (result == null) throw new Exception("Failed to deserialize response"); return result; - }, $"HTTO-POST-JSON: {route}"); + }, $"HTTP-POST-JSON: {route}"); } public string HttpPostStream(string route, Stream stream) From fc0e392be2c966c0cb92b2f47fc2c84193acfcb2 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 21 May 2025 07:52:56 +0200 Subject: [PATCH 47/69] renames job --- .github/workflows/trace-contract.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index a86202f4..57dda93e 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -17,8 +17,8 @@ env: ES_HOST: ${{ secrets.ES_HOST }} jobs: - run_tests: - name: Run Release Tests + trace_contract: + name: Trace contract runs-on: ubuntu-latest steps: - name: Checkout From f43fef47430ec39d55fe80ca4d2e9a384cd54bf8 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 21 May 2025 07:57:26 +0200 Subject: [PATCH 48/69] moves output path --- .github/workflows/trace-contract.yaml | 2 +- Tools/TraceContract/Config.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index 57dda93e..cc9610ea 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -38,6 +38,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: contract-trace - path: /tmp/* + path: /output/* if-no-files-found: error retention-days: 7 diff --git a/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs index 5cdce6e7..1677e5e4 100644 --- a/Tools/TraceContract/Config.cs +++ b/Tools/TraceContract/Config.cs @@ -63,7 +63,7 @@ public string GetOuputFolder() { - return GetEnvVar("OUTPUT_FOLDER", "/tmp"); + return GetEnvVar("OUTPUT_FOLDER", "/output"); } private string GetEnvVar(string name, string defaultValue) From 96d157e788289c4040300b138a3a34d0fd0c0e2f Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 21 May 2025 08:22:01 +0200 Subject: [PATCH 49/69] forgot the workflow var --- .github/workflows/trace-contract.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index cc9610ea..f8844235 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -11,7 +11,7 @@ on: env: SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }} BRANCH: ${{ github.ref_name }} - OUTPUT_FOLDER: "/tmp" + OUTPUT_FOLDER: "/output" ES_USERNAME: ${{ secrets.ES_USERNAME }} ES_PASSWORD: ${{ secrets.ES_PASSWORD }} ES_HOST: ${{ secrets.ES_HOST }} From cf50371dfcfafdac6b886b34c1d44af0b33c31a9 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 22 May 2025 09:08:22 +0200 Subject: [PATCH 50/69] attempt to fix output path --- .github/workflows/trace-contract.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index f8844235..f990d4ed 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -11,7 +11,7 @@ on: env: SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }} BRANCH: ${{ github.ref_name }} - OUTPUT_FOLDER: "/output" + OUTPUT_FOLDER: "~/output" ES_USERNAME: ${{ secrets.ES_USERNAME }} ES_PASSWORD: ${{ secrets.ES_PASSWORD }} ES_HOST: ${{ secrets.ES_HOST }} @@ -38,6 +38,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: contract-trace - path: /output/* + path: ~/output/* if-no-files-found: error retention-days: 7 From b977092936d0f28fc8fa352e0a8f1d4363c618d7 Mon Sep 17 00:00:00 2001 From: Slava <20563034+veaceslavdoina@users.noreply.github.com> Date: Thu, 22 May 2025 11:32:14 +0300 Subject: [PATCH 51/69] ci(trace-contracts): fix output folder --- .github/workflows/trace-contract.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trace-contract.yaml b/.github/workflows/trace-contract.yaml index f990d4ed..b09ceae1 100644 --- a/.github/workflows/trace-contract.yaml +++ b/.github/workflows/trace-contract.yaml @@ -11,7 +11,7 @@ on: env: SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }} BRANCH: ${{ github.ref_name }} - OUTPUT_FOLDER: "~/output" + OUTPUT_FOLDER: output ES_USERNAME: ${{ secrets.ES_USERNAME }} ES_PASSWORD: ${{ secrets.ES_PASSWORD }} ES_HOST: ${{ secrets.ES_HOST }} @@ -38,6 +38,6 @@ jobs: uses: actions/upload-artifact@v4 with: name: contract-trace - path: ~/output/* + path: ${{ env.OUTPUT_FOLDER }}/ if-no-files-found: error retention-days: 7 From f2e0794f9f71124d4ef7743b9aef5c940dc4d488 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 22 May 2025 10:45:31 +0200 Subject: [PATCH 52/69] Howto for trace-contract workflow --- docs/TraceContract_HowTo.png | Bin 0 -> 274488 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/TraceContract_HowTo.png diff --git a/docs/TraceContract_HowTo.png b/docs/TraceContract_HowTo.png new file mode 100644 index 0000000000000000000000000000000000000000..eaed80f06c82d8796a1b7996ab230c337bf855af GIT binary patch literal 274488 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%0AEKy3Y2<7KRW&>$X7UdP;LE zX`OX*qS|HZM76(hIVKC|QXJ*7m`ok?@EQJAdS7<^81u&5C)9+f>ryfhIvZ9ZUUw}48lRk7dpAh6Y%JE|g4 zl?Rlx3R5&Y{SA$#<*ExC;LmKJ#XX%0rxg?UUZB!*pb=}*`pwHbIG6=qRzE4DE>FNun^*U zzMxydp)!?1iyI($P#oDj;>=26;PMmcPF6?hILWVJ$U6nY0P)1y@?8~sB-#W_^UL72 zng%gb4;#D+PsrL@)bBo=Xh-=n9PQD|Qd|efx6uxy*oTcQ-;; z|6=kKVcSY|93WvqjGJtKKdtQ#WUE>mM0k5hc`1~smyMI2sT;aCkvm6PkXwe{LGqJA zpLeoM*V0akiDf)ZKU=xSO3MW3{d3sq_}F~MPBxYDx$SMT__RtY3-Ifdi4cD+%KR!a z_HcYTs6fySHM5MO+Ff^==>v#1w{VL2DlJEku~@2*v{!i3-uU)foyk9vTDo`qLl`$Z z$uB$L?Gk-c4Q#faV&PF&k~i6)f9k}&q9K{JL?pV0Sb3nXZ#cD%--Jt0I>QNu${A$m zs45i3aDRo(mrU0BO!0Z7_W~Q!6phsS@)-5f!}tUKcsnFcz(tu$3vlmj_L*sfNW8Ez zp51)AbFo&(QIx9RQWuD(oY__!xQg%hX{}`B2G1o5U&>s-Ax>_6r#@Jcc9Z)8%%0M$ zq`q=&c5mbo`n)GWpHPocS4k@M^*-5993F_pAxys+W>(4DA!a!Dt}cR(VS~EueGj&) zoN5=0F>O#Od;9I~X_T?wc-luec~fKFlU^4b7&$n1g% z(Z2iK2tq#8CC~|~!WVNo!6{y%+9A8mK$VO$7F$X-syuEc($rR6m%W>T&k439yHzzt zV7W@!bKzbr3az9@> zTSa~Nvbg%nWla~uFpZ8scDhwRfo%Ddvi7D zHdl%kSJd7Ej4s>K-i)Q^%vO{rN^m9YJuEiFgDrdbtnHH|cDZ%l1g_b}G<)!C_%@B8 z1VMTK7Pz~FbLtysRWf7rrOHhIzMU^0`3a*w#gA=H*;{XrMXBBE9jK{x0AGxrZt`!O zbGM?YplQPJxDYnNJs?0sOlp^#!bzx!fi#`jq*L14&t8$K>(g+DRS8C;Nu>JBrPV?y zvAIwNo8N5S z;ngQgBTA?QWIK;-E~{ao2U>+RYExK={%>Zcb0pu&K|EA7PgB;UJZc56k8uy%d(gIn z806(L&jPNzO9!Ww>RhlF?+-ZbCE7IkHl-g>>AOAsMaA>WcBPGb*yQ&610)(N{ps!- z@*^ZV<@HWCDcZr{U+{Pr)E4)--07JaDgUzeVxPw;&|W`({3kNoub}atgPPmAg@}Kk zwf#J7##^Mnu%`QOH=KJA?l7<97C94inE}LE^l(K$hC2#DM(+>?PL)t9Wf6n2cTRC9 zhx%b#FcEytU~wc6_BQ=Wk6UJpoOSU3`XobPepUPb3@e+#gHz9^4mTy;u`Q_0?q7_4wGSbZw`X_~jc{oH~H_ zbcdxLcq1wahY3bkq&-|l`KyuIo% zW8}Uz6H3y(-H4?8q=g8%=SYWkY}dt`Z%#v)Cg5Lno?S*bg#2b-*?`mdwVM0&;%Gf> z6fEgoCAc_s{3~`xZ%Amd^YqMNEVUjd5_k48z5T2TlE6vr(;*BHgv8#$0M;6u^A0)? zgG9^nY(11sJNb%AN#^A4Y7ryAG7;{yAa*Uc*x5tg<&fK>09k(jU*}Ey4A${NWR=$* zAUQ?X53-Ndxpz1eZ3E&1Tny6Qbrk{vo8)&&ZccW9A9NBuk)=6&kjw~=r9Bq#(f#yf zpYRug1d_WKP1V+)IlXOb{LNcCE;bJ+mNLvykU-~XubErxYmX!?#$+KyT9}Vt_iygm z>n!0QUNlBv&uCPW+Mtrg1ejk+3bVT(P4-E{dYr!|bK05M(d|QKeLGr-gJ6t~ov0f0m7vlK&zZtBL4_spo(su#n8+ za>eJGM?>4;V1->4?cBSeX-!t`D(WY!WsIVy($XE&tk)jkY;QTIm>m>X4k?5`VdZ_W zTd}0qAH<|EYYcyB%Z7d|77>nkv6gI9unM}dw%SibYa9>IdXBl1B>_Wbr`<9TGWz;d zyIO!lJtg*bW>vrBvI&-0Tn ze1;dWqhkK=fcUqE7=-ME|-Z!K{f4!4h+2XGT^W<9F1(f3G#GS{uJ~!ealyg6nGH)$s)cD?04hA=VL}!_tmeokvn;H9H)2bOOvwC@$S5$}d)y=H<01I-k*lA12B&w@WOoz2EcGL-j`K@WahEP~y9*tSg#9~tjThk^Rh7P2Pr_X&?Hu%T6QHsnz1#qMiEH+SA^Bi06}RrGpKZG*m-K$}%z z&e|8<;NRINd+>{(%nQyWY&2iiV@_L>B>HsfjeuK76RQoACMe`4D>!7D*%kze^$2U) zDIUCl{Jf2wp(n3adE$n^QMV~cnFB#|i_+v4)n}udQBzF0fXpmYs`N(o;_tFKT2@Zu{D{prh?9|Q*xx{(R6P|!a`{-j-Evjtc zf$68vJdkf?`f<(udWUnf#h1m`l$I39rTj-B@*n5*#dIv)Z(7Q}-~7;DaC?uepA@%8 zeV2zpa_HltSbqOg5hw^T(2LCe)}6T&`Zy(|$xC3w+zB0!S=Yy0m{wec zq}O)sFSpK8({70$Ajnx+ZR0!Pak;n;2F^D z!(jvz5aD5VZhtgFD=Z}?qy=%7d|55cUQZ^Zk1Q$7lrs>G7PCK6KN)WI_8@h&N2EwGSdlth`q)s$nOTOaR= zxH~-)@-W1Up=U+gWcz)cae7uv60x2#4Np$E5lTg@+jX|R^Squ*ymRR`=vWA;ouR~y)V z+_AT2$C$Q`MKr|*y!6Bcasm{2r^p4Xq@<6=!F=lbjCKFTm%h9IHWUH97uTnpD{{zf z;!e{z`$DMmn=1b0s;Itiqnj;6lT}-UG`{U5Gro1xo-q!rI@Z7K+mg(5=cG|Lf3}xN zy9EQ?tCHY68aDlqbnL;UJD($@XpcujG>7`{tdg3e9QC_nOujIKvB8feM6e1wrnqM8 zI2j@dYNG5kvlsUpnR!P%*seUKU1D;vP*A4;p31PuAuqAiKW&%1K5?n~QWq>>)KQ8? ze8i+sivGF5+yX0xYLISt{&}4V|JJrh1t|HoTbi949byI%4Q5`w4{G zqjG77kE0&Gw&ae~(^|OP!9E}; z#m$W=R^|^BymyxJu!Le24&TMimR;}sP&tsl=d<%o?Q=CdZ4jW>V|w_xnkWz3#neT) z!UMJ^JfEZ-U`bZ4Ue=sW09u;W{aRRbq%z!rew6=k)7*hXJBAr%M@eZ~TGKiCD+-yP z*MQ6+z#{P zBzUN8;X|IAz+^H0V>AU5E@4Vu_ zq(j+xvCqJ?s{gx9>+N&%R%|@R%?rzT$kjH}9?fvPN~%_I2(I!B3y5xgf@c@0SEzMs zRTJ}6Yi~uzO)?Eq+4?u)PCGPZs(c1|RJ;IO<{OIMzhi^oTHxkH)%#MbbC;^#?Ng;% z$Q^18i-C;1iYTKMCrOqc;>G))FZ$1P@+MGlxbLv*NLmN0IU*lWQol#!5YSUu1 z3|{uT$w}Znl>>ovP3J_ppz1U2uPh))vVw+&nZhBvqr1L#GZ1U!{)2VVpsFJ_HTnz5 zY)ZQUIVj-0d~i0^s+2f;O8xaVzCx}4zBNF1@NZnUeO20gU;n6WXxO@e%|6ZuF=DU%3u48BR;BB(9DTFh5`)3S(Jf@whJzcw+dLT5Nc2- z7DkI6*ExhB(}c0N81ZIM1-@Hje+qejf6vW_m7f$G5Qdrdlqp)EiL+0)G?mu~K6+u} zxzli8{HfD+PSNH4?6Aw*J~#Qi|N6YvqF#9vn&bqd&fd;x)K|y28Gon)QE2yzvJJ(wcq1^&<&1jKDo6fC~Ex^$YO%i2Ju+c zy=3)EoHMLryio`bEtsWccpj@LiY-MzvB`OysBHW_5w4 z)-JYrJ5t$G3%tw*2>d)o4x_4ZXI$tSk+E#p1sREH3h>@B&!Z zXJkx1Xf^-X7rEU#kaP)t0Q=oVM>xc z#@JKfRCjA8P;Y(tVUNasj^nu=y(z!ZadttB$-}AThhE+j$pZIBXqi=))NBI524p&Q zG*Z{H^d5R3l)Z;6ZFL+-U-<82?-R+;y!uS4e#y5V|-6g`sVWao&95sZye& zkw?x6*>&4{caaIgc0Tu~U-AcD7d3rD#_6oF#)oEq0^1Ig$$@@!xU`8D^Oh`A#O1trukK^&nMb;bZ={)UFDeV^77M}Gs1T@ zlifur%9=GF66~5+VbM`Ypej@Qe?Q@4Yk63`Pj>o8w&vYZ37apei9#JMydg~bAUc(7 zvtU1Wc7EIEHP?~$=51w1ku8@ZO(&F<^uIm3YXkJq1rYdu4>;hR zvSl1PUDf9`a?nAg*zx}4Lj6RfhX3+!Ntv|8sZ9TTRS7=xU;m51VE5<)rr)w;s#ilp z@-s_h^MD?b2)H)3#xmB$mX65X)RKL*+wY9S^UbZkzbD7iH<4vc2O5lx@XFnpgV4;D&E{z08?0=mq@OnKvNL^d^* zp+~3rbu{_6*k#y#yNTS*x3cTMFRn1!_bg)Aw8Q)U{i_IeSZT;eDc}o?SV=r8aQjk6 zLQvcOZTb9AW$w>UX?$$7`vruI;xk=2zyYj{&u)A`d+L(9*wE+-7{hg*rK(bDybwrs zw`BRes!4e@h+QD}6!*r~DYo4QswkriNI`89B)iJ_xgGQ6?`Q`a=Tp|R1`dD?FmfXK zp7SeP&aeM>%8LhD=vB@sbQ^ZRS$x==kS8mKS!0Ojia|B$C zLO+Q}JCrrXjmqbv47~@e>#fzbPQl|-gtXiQ+#KT}#^pGOo%x430$J9*GkFJX;nRC` z(9ZmndCxFS&5CEk02a;s3CTS^zr|rz9u6?cvCUC)e8^WDG8TS?MMC3e_s&*Qd2Mm2 zshr~!+1vs50@^@Jw$)D-iJxV!Y?ETN_oy42V{!oOq|u$&=9{gUGI_=8^P>4u{sDTm z@Y^xl0>gFzlPlg6{Je&mN z(E9)ItzfP%{NTUV=+wUt?++wW@_ZYATUfBkPf*7x{~7AIa(d6m`YhAfkJ1$W5;24?sVTirtt5 zoxS4x7fxaljQ`uPkPkmF4x3F1H2D|QKgDaN|JC;Rdx-63D`)@0v@=vH`=`CJ0Y#)b z<~AVk@7uT{VkU<&c3e665A1&Qt<}2q1NqixQjlo3@BRkMrX1~xx8xJsfmRChJiATa zrZ=;D>%mL?KyNhQzrklPK=b-;jJu^=^P#cIv;H8E{I?0hsrbpOKo5`0Tjn+5=RfJkc)n z0Gy4m5MMEP(a_$CMtr0dJM5^IJW)Jn!_!xt<+Tt(veR1j9t!%?yT3nOJY5RxN!cy0 zf2{~Z%|j-YWow-qlm1qd}Y&>D)r;jqk?x+#I)} z2eLgG04-e;x7RzIV~3YOg=ZiJ#BH}%{$pJ-Ji}w3?m^PftETlhDUfY9(T1nL8vkvR zi1>wM2mjRm{v9DGz?8Mg?natIoHXkke9!X6_;YQ9!R7vq2XWg_CP~%@0*-Cyz~%nl|5WTs``>}!;S7xNM`iNNMu6{;PI>Q=S#R|@*e0=F41ExTV9 zo8;UJmr_cTAIe%{?~A?u!`i=c#*-u-7HrC}b&>!|USe7p48{GqGvVpIU2<@r=3^Jl zMO^Zol_2n5*1*Xn{2EN}zvElGdWwDzBN!_9a-fCrQv|~~KzjgDRej)i?}vjtNyahR zJRZ~Q0??tN7NUgAazzwn&~{}G{$FSTugIEGD}CsDW%JGV-PJOoE(HXl>N5vZ&fB%S zdj+dkkPi@g<1-^4aA6Z6k*pU8({pMwAFN^r>c@^iKzmk5_?6~=@Za;j zulU=M_I%eCBC$n3#_v_cO(s)I;=&|Jr#}ESX(!JN>E&JKbXbEyr|15F&H#YDm zzGeKy7dTvrZ}Y{SR%s}9M%C@y?1@+Aoc&M?nO^9fsQ5GRf9o5h9gc!mMxBO*?S-Kc zbu0$BNo#A+Vn2~GHZtFy%$5ef1Z?*-s8E33IUHhdaAMX=p)+z_lE zvSlaV7Rv+i@f4QW2JQp9pXJk0R8}t9G|^Tqwq%J|ZLmO}mnH6O31`b3jOI&ES`PalO>vtMTUv}A4uuYXE z(adT#w(rPXJpqG{{;(#vhA~gf~L@gpzqp||-I zRo98g+qWcFxTy-4ki0p@OVc6<$lxCcTZ$(__@#fvjDAI7U)Rw!?y3)?Y>YL0mqU~> z%#B?P3X|FrQ_lw+jE|eBjraSPO>H0+R>>DAO`@+SfOr3ACNk86R>4qk+)-B;2DSxS zB!4Jt@dUQzS*1>S*ivX*y{`k?`WL|t<}IcVBu<)4l{AP^94qs_C$>MlU=h(LzWvFV zeoQ2kk25j#bSA?o%W%va?UVh>Fd_(ciaZ<>fvo;#q(g+wC+p1_7-T`>P~!7(jY_W>u6$|zJ;nkGwwT?Vll&= zTc2H4E_6>yLXdtGR=wi5zzNqFfm;QuON&X&MT^4P5UV}r`eIoZ^MN-%15M8zF#E4) zTLAqelnw)4qV@RV3$VF@$Pf~i87qgt75KwBW}y-t9now|EUzd<@&dsmzr@rhlBYmZ zrK>4cw=uoLk;j)BXq`YlnA;|*-PaBjbjgUz=N`ufBwrWWHprN$@3UKt&l1n!O5(?y zD*R$tG68R%%=8Cyy&kDABVQxhmkX48R-oqKTht1lUql&pvF*G!=BV+3jUXHe1S2HkRo&*@;Y$?vki18K z?)W9yFf6IZ$T6NiHS#Ral2MB+_}xhLEn0NPQQMpU-#}>yG-JK5$(3EwEw{4SdTdBf z4u8)tMzI~M!`H3mVyfS896jS!P*4S1R3$Ch(FrXs>TnmiwzNOFX@-rV2VTPe8V;?3 zpL)oF)fKfQCmmH1_WMF@U~@6mt0@|Dgx@X(UDp`k_R4&Pj#J3B-b0oDU_KLwx}WST zpQ`JRIx1D~Up?{`q49?f>w)f)5fe*HXQG3UN;q59$1}oLAddj^)QIWZ)0r+-x$^=$ zZ$Ee}>@TV;?@GLb74oj+hkLD!9{YFh^P^m1Dr3CaJW2|jO<706pNN9DmOs>JMr1ke|aSYERA)CWR zDHSl9*DypsPBeZfOMV#$^_GSDca)xWd!O&XkYFxqqNQ)XcGPh7v%2em`l3_#)ZJzb zV)&g=!YK)hSpAQcvBP`=FIUU9ZD1EO1nohI zKe#X%FFy8+G-Fi-Zw_#wW(p*VXJ{tkxfkcu8oxtX{)4(avXEX4O2Pvdi8gd@EnA@1 z@ias5=v|HAZ*&Hj{GqpZsdgVW%e2COXD2!Q1o9GB#f$`QvL0W35dG73|EM3}WPs9i z+ZX?v0M@nwSER-^?kAsBWelUiA0NW948V8VK))dy2rPgP{TG+9eoX|iF@;tC*m}R^ zyziS6ad;(g%~AYd(di2GNG*0pVN3fDtnngntvJ#El=3JDl0Eo0RJ98uLE)vZ&F~B9 zlsdB#{1*q0#hhd6Mwqap5H-6@lNfxOcGcScGiq!s^zUbYODoNJsC`q5z0(YKRXhE< z|1JO(IMj&&`K(aO_^u*!)L~AZznEt2j#Bsrb z<7PN)Wmm5mifw`%TP*7I>YhhKgO|NQtD~SPOg-^56 z9r$_(Gni&*kK&l~5tN9Tmbp3;5z&9}6)JD4)>8x1Akfzkmn-WPX=|sbfVxyv30Lw~+I1>~ zKT3?gH=h}_04BTH>vHun(2@HQa^Bo)>;b~OC)brMpbcGrKJJkUjbQ3lVvqOniX8hQ zR?}V;fVVJcGEXpdKX0JBw2FViJJY;tEdN(7$W6u<+z)wrTHRJQ!5I%{0erD!Ke#0G zn_{==P!9io0-kR%_aJ`&y}y4Z!mavrP2%M#bHtCy`u@9@jBf|$yB29tf}T%V8^6F& z@lu1!a#IoJ)U9*Ib1-y2xXFB^1h_CyM@If>^wbw#@3-Ie4b>lIj}$j0Ewp*w;+V`m z(f8tK&7V#rW`AO{S*pru-BkhKQTuT`=7w{tT=0R%=?jX6%UKUJD;f_Q-lTqX8-Z^8 z9>A}UFNHpYu4`j1yf%3UT!IE1=w2e>ICst?%|_>4<+W?&=q~;U*TWVjQpXQ8{n1wC z2)FmjV>oMp%ttq_sWtn@emE`#G{Ww+`(gF#VFchXU{zqCWaCBtOI0Rvz%fhchT5Qu z8i$nV2zOEA1e|HA4ttW)9JByR2F_wePtoQtr1|1(X$a1m6)Rh`4^H+R-pxn9#MJrL z1QT|8k)f*_KUxv$refu$sFkV%Cy?)U*FRotliqZ&PJFxZp132y_-mKzCX&I4Ml5-7 zR}^w@J)JQIs6vVFAYzvU;An(y9 zRqGxfk_#W|8IElf4$8UqWCe?VFPO3`Z_HB<6*Ht0qOa%RH@VhpU-`w)#H`+XsoDHi zVByC>lV>%XuxBt2Fk&3BBxq{eJ6AJu4`0t@3|uqV-^ui7;^T#U02t?Vyo80rch+x5 zZ11AUh|17-=$rTQD|bP|o#=n6?T?rD``V=n)L58qPv)VQ8HokZrz-e@cc8ClrK-1f zFwzlWXtt?PwkamyBg0cGNkOyy+F`rN4>3!1G9`Ot z+N?x(mdtyhrsk%9Q${*Q1Ce~gvGgM+9$ET?&EbSVxwpa4I#W#AEHMNBMQ8Z+9e>n= z4j)B(>*o`|p~655#LF0Zp_BMT7`5HA890g0H{Pmo2mW4e0!oAf=PHtV76DA;ODE}~ z`)}h2X;w~1Qm%z4xlZSn31#_WYX?)Z&nj@?_5`9M=5(O=T~jae{I=HcttRfpBgQ{gLn?hpBW%yUC(UELLgsJ;b3;RiUTAinDF&E}(uzrIf={+w_(Qi} z(wtq`{ZW!;{D~DmBil&t2*$IKXAe}K5j9$_@dxMDB(@CX%}SPqnE;oKEXEoj>ojR5 z>e@CAwJi`t2dj5Z9KRr88;D!94Q#>)oaONSZy>{}Xa0{UZxz?RoR<`$!p*ie_UXz2 zCl(d#G&u({0&iqxKFrvJY!lNJ}fD5g>>>O_nV4K50MsPGC9k*#pd%bjX3*-s^8#rr|zPTq-t9L{%A0% zfrWe;Z|^#|=JH~^zO5>(z_z)ypq#yCdtvzPl6@4cm7gY8^qTs${fAlJz+EJp=(xd4;R* zO7ae0neo#Oq#U(llKYzZM88VuOwb<0j$N=GG!q*-_nub6pwF=##yxd9CU~{rbbPSn zw~3$K5BCX>i9;lGL4j;lQ|!ur5N)zK3F=G~3c+N~nhmSXua)he^7*Tk03;wrg_Dev zdE?u80+q<)=CiIHu(E>3WArNO#Sn)$+g`S-1D=9x`_Pg(E;v|`_282^J&kk+!si!Qtw&C3 zCD71gs(h9VB1f57h1@$Y4*$`gaJ|FQpZoK7`uGPrwp6C#$l10-J#Zc?P@1q4A3iP9 zq$4yl9^bj0R8{e;Gh9}H zbBmy<8Q9#&N@B$vt?3+YG-uP1GGYbA>hs|Ao7x>JGOv0fhu6R|eq&qL4{Plkx4NILA|X8SqWP3W7M%Zcz59a6_AFUWdSEw zV^2`RKq~oKO5_L7T1hz7GG?^&O?DGG*+TvsA5n;;zPX<>0_f`s+LstX_UEzIQQu_ zkuiPrZtV#1mUrc7zLaO0m$Q~*D{8Sg!kVqZjtx$PdVY^>O76|*2oAh=ZYxhV#Vn}P zY+BV>IsB$oRJi9jFV9+Y)0UTP*&R*Nn_BUSE%?eCYc`0iuB}f8uQl0q+Esq5Ip4Ke zlm5e(VvbiqL6_7}&kcM)FF!Vv>mh|w_a5WdEy%w1mAm`>LXuSp=N(>`c9=6Ob2B{r zVlo4_@KHM6r?10DMW`DCy)>VQRjST9V-tEzK1@7J>astL?<9zMyw)DDbPiN#zZ%&rp0wo_9DSy@eO)YNWe-@#yrbk#}UtPOIbN8{kj5_yVy1S zlEydLhEnEh(Mz^c(l#+1aV*YTuZ;q)&2Z{IXM|jE<&uplAn6Rt)lR_6>i_Ylx3*0~ zPR2`irD25eKgv#t|p*#?6kQOz&q+5&1HHKQgEXQ*#hYQ$rb48MT`24wZ ztuHtp5Qqu_)?{w2#I9!xs-Mlb*$?LzG$o4$g(YnVb-tHh;G=vCWq%=`G8R1H?u|7&EU0pbe>;k?y2nj@kQ8}I$08!=athFrfcg))8dK; zCJ$SDLs{GhxS{f{8{Rl~E`|SWet+v?1`46<@j|6;e1YA$^EGT(p=)%J$Bfr@*z6#e zx-<8%nPz~NQm~WrE%iY0WnSsp{X5UxS$o9VKx{HrSo>TWyvkfgcT*TjJ%uW1an-Xy zc9<0!ON6&jOnrcBtbZ9>=b%A_Y80Pya@X1H>r+N^NlG^3p&s1MTV37sG6C$jIb7+4 zUpIBcv4K>rM872mDDX>vP>_<2?q2-DoPbMKdJ&--_@+rK<5)QORMMig9@+J3WQcZAZYU=`?v|N1c#b7yxU?WWr`*Mr4(Rr>rm$KQ)Ol4 zo5_1quau0FRn2U7;&pi5nZOZeCJoQ)4Gc^Cz99vs6?rc5@?asR{uNe%kA+K1qhab> zvjH#2vvX?i$3+e!lI7AKMR(Ffh{75Es(jP;K2`sNSFU6a_Qtnq)WjDbv%^Dlgn_AN ztFZrgx+3kz>jy5*Xs+=-o;D3*G`9I1>TygO{%xS_6_!Vf15wPS;(V=MigE{|AvqE1 zH2JMyp)rJbnJu)U&v`Wnh$yb&1cyrZN*guc_p->U2D2_fDkhQ)t3?sP2z#DX3U?CC z(BhpWCUz|2S=+NG-?`HK!OVQ}8L$^*;_~UW*MnvQ;E>SLBht_0Pd z`-XwnIN_sX%{cMfy@wp4VS=9V?>rmJoMvukwnQ|2PkjwM{8k{E1$qg8zxac-p=x!; zhsRV1cO)4FZ8EN~2maw5jX81*5_k;t>gc_S{lP&&FK+C>%J}4KSok;1H4MpcR%IYr zn@u(a78sV`wWQitE;T=7Y6aod_&dpNzF(cjMRitLe<>rw-^+ zS1I>mvdf1}K?hxwInvxeML!@0S&BIPQ zA5@!xIUfv{s((pQ;9GdWmz3@*0t_+Bp{p?^_hvk4D-gKH#q~AWEhSW9!=aBo7;}VE zUnc4=;P}gzhk|0OL64X2+L)`ecg1jj;)(O^Op<@y1X9%uF)eDuiAtr!0PAFG4e6fz z8raNuBnm&}=m%Yr?GEE3tQBJRw@w}U564)TdQI9i>pHSHl~Yg0u`w)Hj=Ys3YQ@dm ziLb>*4Ni?BAB;79?i5}8Zj*558~`P(_#u{iAk0B@H=SYcy;L$KvBGsoHl}2yd+O;o z;VqA6VDhW?toJ|JNStA2)?)q%If{X8N!oeUQ{q9W@JT7@Cv?_iGs&g|iF35P*1lm$ z@+p+#GAXQFH#hE!srCbov&>XnW{v1(G;_|CufukLA&Gnh7@>1)&e9LMu=v!&WPPLd zcf`-4+r`%IAhyieDexl>UuN-)MN1jm4ACjS{ZOYc1JSolFTY6{kGRjfFNX9R1n&4% zp}#mA)S-_xo-%;Kp*vj3B@j$1Wf)D(+jL7hl!Bq1U91~i$aoU_G0A5!&5QDW=)<3Q z{@b#!z4Q*2*O0pyONI6NkJ_>K1n}D3;dPh+w!z)xb-wnvi9;(SLbhHtOhn$*Y5WQN z*Q4jo@@f>3pL{T8B#@M;3D(3&vd`cY4R!6mY?u;JSOq^OZM8Mz{4Hm6j?(S?B_%>) zzcZ!1nNyZY8sQp-cRZ-U-p9x!9klv34I2mGeu`%@*@f)bCm7#!Eh`i?=1mQ|A>O^E znna({^aDooi!6+=dk+YUe9ao02#Vlqjc9te6AQ?$tz<4m#5n$9?l5m_WV~8ul6I$$ zi5HN!R%N8^KACmdi1OM#SCz;#UJ8nkv{YtP83;&_mk>&f;}!aL=CLxgByNj3JgAGf>E?c`i3%dSpCnF9xQusF|dL`W5Q*b2W01Pd@TcRy7iAey_u zGMpS>$N)3SzS;wqhK>`U?4J}~lRCTln`>TUc&XPT-3XX7RB?~%MCDS2kJ&*m4>%4NpPc&Wl4={SF!e)c zu{j?I-z=ZF2o#-d_@`~j2BuJdGOW*pusv#vr(_v3l~P8w!VR|9}Qrt`L zcYU>ajx%ee_ToV)vS-~JZVldnVW%?lJkjE z*qxXbV!^7M&E+xEyjfdNO`H;5VBgW4jSM z;YUim{=;oM;BFlf3Q2ej*k2`O#~Bp&8J-sAI9!-M)w1K-Dt%v>_d~mG)AJ~GbGj-t zbsj2CPZsCYH#}-H9EATd|0PuetfvO}G^Q?}6^4E6EP$VQwRZR=mwt#tLloWxnNadf zfO{;7erW2DXPBg#;A1@*ot=FR0n`}jk+x`UjbXTQ zSE4tgV-kIP>Pp$40Y4g0Z;dzC3n?5A{cg>*JKfZs>B9SE973g9WVc`~tEG{Rn4{cQ zEx{3(mf*RkhA$shZ5BCwB{;;ee1VEi_?38Hbd2Pbw-4alj^UW_lkm9{hFEl#vY+7_ zA5hCUWdF)cDDg^@k#p6{${z+&lh4D2ccawG8lid^nr{>0D}D-EWRvQPJ`JVJD-gUq zy8==LRO7?Y!^QK~Bwz9E2hUjMPt|&JUrS>6aq5$|N$nKv&%_eQA$Y1!x`m^+Tl({k z%d70Te43a2M`FbLQPoxji?A!3o3FaAGD)3tzQ)ZY;p21j$Lb4TrDG7ls_Sfc6C7iz zIwN8FOLuYqSzK5)w9+4QpnK~1=f6@{Ajds=+p`{K(guNVfN<&FO)te5_2qeKi(p2> zI2|yVdGFiw*|CSoPmICPGIu@tbj-1z41rPN9c!QBLZfprqcD)4QoDD;#wIC#+Dyg_ z?VI5;bPV^Yh^sn_MUNHhv=ba!KG~9m;_EVi?({5|Dhp(tTiQUK;aG2@AaLH{t}5v` z3p2CW>0OAVlzosxi(1N988mfdv11!m`$cwb>yw_Zd&(F>fCiwVUIV=Ff6k&HK^U2; zd*7nkfcF$OfPFV=kOR#Inso-3m>|6v!?vGE@Besw%(txQ%k+@$HM3ohr~4R)O0PxB z<_I(apZ!0Eei?)m-TCYXR_uC0J$TQG@kb!O;sXwmkz}zx9<1W(Z=Txw+~C&8n=tIz zi|5YSUf*ThWSBPGM3ui=E9426453@ib*t_kcy-k&00nr%CL>e`|ENa&9vb%wK4P#f z`h0Wf&m8{SbRHIEkuMBxKb8*X?_LphH{zd*L*2y|SDnGkE_RY{gv(NwSQZv9NL(QR zsJB;9;ioc|D#P?rQdzYxhXT2D7@vZ1CckWa_jIK-H>xQ|K^}s&)Ti+0F=b$%prsq2xrR*|@aHJ?fq$X9@M~~z2?kaD81?QeTggOTKAP8hQPJ2m#EdN~$`C5MB zT7HOP_l+qf)5zdfX_{w1nq;srsCcMjphvPMY0Cl8^L_B%8*%4L@0f+kgZ+;C~nkupk*V}h_mGZ#~FYpMpKmx1$JL??RLX(jM0 zRb=Ez2!CCWuFuwhmr%3P%J6a1IbayH*Dy2BKFcAiC94xHzcP($mCS|l;Mn>OHR`^i-oXf`=#EI26-wv%yl-$ZBm_CoUO~In<%h$Q} z?KAi5T1eXYx<Uflyh zDn-}fB>{*`{Cnt^3Vx`=H;qSFMc)v@Q!aS8YG`bzR#J*vxx8!F-dijL)o|LCey8T&7gD3&CnNEC`E8B;zx-47u(@e+D9Qt5vq7Q zJuX6qD);&eug!RDTk4j_!FG>B+y*CRkyun%3?K6Lm%+f<%ZKVU7Y&4B0`r+1xZGsJ zx0(;nD=VMs#aEr{RWwLKGgzqo^znOfSa686vzDgeg&Zzy4VdgJznN}BoPT2!@P&E* z`54?$VcLpfF!?#sN1L)WFQh`C@QuIU23}W6EUN+6;ejm}yu(5pEeBDOiK=*Xn`&NM zMjnulpJ?fE6gQyi@zc))0EVFfXa@BRnTJrXDuuanm5gGqg9T)Vuh84OyV|H@{64|Ovt~91V^dt( zIg9P7s;o`EEq?cd*8TX^Uk2n_w}TcIL9W{v`{ozt zmOd;mlwNh%epcC($3swwJNzDtGFj?kDV)42;XbuxUxtX}=ziH{lv|6(~PN9(T`&8^o3Yt@fw&M?p@f;X-dMniXPQ7 z3=9f#j;_bMS1P_X+ znOfa4nS%C*(vwU3&26sF(icunjxU5*eByOY8hHY^-~t3zVkpntnn4dCm12vAmX3}* z$*ja42B30U)u2K`$66s*gAlNO@3%$IK7}o31pnB9aGG;az3g1vEie?d5*ZZq5e96Y zcw7U$hbGkK$Jp*@Z@-~9?t;2rwqIuF0LLt_B@^AsYnb?|^D}PISdYU$>H&qOp!?&= z2TZJ~$;Dy&3Jrp6m2uyW=*tQ7U*&=Tm#rD-l3i>~Np5W}u-F!w8uz>0-`zjm)jwW+ zII0f+P0ZE^;tQbIGH>oM$DQI=*i5LBSE`r&9gJ(8UFKa~>b!Z$p_ zs_)(J7z@dZ_~V5o7uL4vsm&SJ!HK6HkR&e^ZW}kGwsc-G4*v)Mb2qM?gio4N@MRIV@Y>#i8PjL zi?mzkYCMXS@~EF*T66JK=Y=E5X2VpUQt>O>R4=X(9(7<{QV`h>mxY zHQtK!g_>@`Lk3F-i2!Gt;K1&b6FUOo--3Mv$aK*WAnEAJM&(&0-;0+U^s@S2`Pv%B%zA)SxIA_`I}}kZf(?y#us?DmjrqiNW{0e1~^$JQ3P4> zquL=GEP(!m)?+(+i4s0B>sei6tDe-!VNo6Iv&}sCql>o?wgR*CasND@tGE1!Kc@lp z$*Hz119Z<3M@Ma3rb4NVMH*3Ho=Z)L*%5~*$nuMzEG>&uy9QSO4+BS|Ig0Lb3s0oy zQ$Q;fOs|BR-|9zCX$j=6yp(nHt-c49Xr<}C)W_|yv}I^}7(KU^=;moZiwlyl%5~#f z7tINcK?#IJLtx~LDr~?XiY&|%Hvw-K+4FX<9V+3@m9I=IkZ}W?OE1e~L*VugVXs>M z#AIr=i$3QEgb`fhRa?jhEX_S zzWmUu9qFexYX{ySTa>6X)fCX#HY@eQ{nHH#iTd&}Vk^Wy@+9VI7qW(3w~GudKJ}Xn ztcGz|e8MEYN+Mb%J?uWQ`9t^Hps}IF+K}NmFOGP8edDqIR#8*hA~T2gdJ|jaY>M^L zbj3s@lPq&+pV!hBlPa=1J|BJkX1!?Pr1tz7G2q>@R@p*J{V#Ni+O1VdLd4J zS$UO-J1gL}ZBX%@O_OKRdyS;EMt1WmIui z;Vr4bHpkDLN#c z34dhp$QFVHyRLnm{S%~g@q=rw8m~`*`ggeVWA8`W+&s+hC|I}sf1Gw9<4CO&+qcoHYaul- zpHn`J*z<1ubTrHxXRkHzhIMohUqobV8RZ$cza2`V7c|x*g&%bt^1jx3GJLzh1#$h3 zjv*4@EN5y z^6Hvn+B}}~v&c7E@3$y|U5l!Y851hxb9WoAAGNpL_j2a*!-d%4ZC(~`YSXj{65nhf zak}3hHPC3vwlUV&Z0OhZrUZRIEB<0muK44Ao?v`D;Cs4yB-)S5+?lSk087Za1ynf! z|FGjv@#50G-O52H4yd`f01-VwI5*Yud_8_Nlm%OUgLU-$b49=WOEnuWuBPVqCh;vd zV!Y+Vail(t&Zs;P?cGO#Ul`wCM&IsSc>%31v$N=5Q`_X}t_zPT;~54F{{GnLS(Vsz z1@l}fNP(R92?h(BI)j`s^i9ndWrkOCr&^026=IF@>CA`2bC)M8uYTam@rQ>+0?g;6 z@yC2@@hHF)FaDh={wZl(*(0JoJM^OFE3G!t1+$d~Z5cN}C(t?n@1Xxv2TSW!#Gi-C zyy*QIi=sQRc$g8gKh;Kf`sAmpiqZ$2Fl3+Z4R@SN(i>*Ol|Er5#q zBJAJ1^Ddx0S_3VLk9&nxwDs=`;~Ti6k9^LL5lIIcC!+!XQdUqlXLp#n;_tDdC7R>1 zD-bc90Og>d67>cj!~b4i{wa{9r|woIyF!67U|sFML+YQ_)#2HV$YU12KjqV|Dm^Nk zlYVTc_fa}}uXMJhFqze24{xq7Stn3b(z)cBPbOh!wKj`=9s-rNQuOcfvfBi(b^mKw zOH&(t%J_3`>H{KN7`sOb{xUazaoEL^NxE!7Sp$HL;Hp%&{!FOoiQm0iW&ij_pcV4BPcKl9 zl*24`v?lM5?ymPJL=*#=2o%m8n#o?4cbZ(Q?X2#SvQF&@D%|x3t*`c)!Zw^v1I;_1 z&C;4*b9uU_V_K*}*gWy#H#7h5k6y@B>XNn%ZI!n_ckT+%nMs|oAp^tB$STIDeIMwI zCBL-c65A<{n6>KvBre4q`P}T__;-Ii+v%vh-|MJEG3~Op6A={sU*Pul#R8hRl*1~6 zVUpACQjf=jcRMndG^~BP7~g}Oppn3ed+(NC+Sj+NQrG|I!+{+cyZyNZpt6^P#;0^m zgQd}n7BXfAsvSEOlXBmWKQVZByTX+Nz_hmB?|5zex&OcayzAoepYCM`{%;%nIgIsK z)}D(t1O{CL(B1#NMnFXFH14lo+nu2(yUp^ozf!=ztn{(*Qlii8P8r+IcgW`gU z;p|CU$)#X{>c^eTsO#*>i7C~#%Wr}%(f@>?Ut1Hj#(H+vOFgxFdh|yGAL1-jxZc1s zmlO8f_7l4nucFt4Go5l28=%^s1@i2#%>`KnyMUY#VZD6(EMyo={+87GrXn#`f{0RW z?ED_1HtT70&*qLF3b|eVPKM+rGFc=aGj>=qrvXWZ=?E}f%tB_B7^88Azws{UzFW-ZfyEr!IK z)wAc*GmYD}Qu^yh{85JE4}7gV4Jrdz1DM4Mw@M{~tgV5hvRfJzb2w(bskTSX_&h;g zCuRv{subuLn0nr(xH&|9&$Cn1LG#$tp4P)t`%H`BZ#?v<0S`pr z6vnb2SjMt_y=U%H(VTI|AgThddXd`&@4U!3pndoN5SfA&>i(MKBRRV-G0w@V7$LT~N~2$_3K8MkpK3W-;`|;a6T4kC3nr>+t)pd2gxM5QfLN z*qFtnbBEhrv!1vw*JA?P-aJJIz}87DR<{QnAzTF-%6 zDCxe%pmm(APpb2ywDAzS`iqxI=14s-d7{wW(A~?*;akKeExMW0rD~*Ek{|l^LPtgY zO1atafw-Zx9F|^oFjUWU<9J-q=>+Vo#cg`}KoRn33w4CCs$ory&9dW}dOTRw+wZ?D zl4_s_Q8yBR>RAz4pIX&wmo$^?j|JIUPP4(#C%X>wtgQ3%0e zGw};MI``FlYk9xo^mw7aA-U|Lb6)%eG%%tYlI~KnGV;a1!9+kpgP%-zQ;7F#rIsLL zj(B@KFwirUs^xO&KcHXM??d#3-xq%_u_XuI{5DYGc4yH@=vMWUROfOF;ZMt96jkL= zKuEOy(OstxWRx2KLe&PKL}@MbDpqAyu99cfGK>s;l7(})-T^F7%j6ddx2pm-y$!Tq zV-E>@)@xpHMAMh-R-(4JaI(;SO~qz-qE)Rdklbq=e*t2#>1v2Z z8neQmkyj_)JbXw$mi>V@CxJc6Ogi+Nmk^hlo1@pA{RP9`1QpQ>N>dc|$st`iJ5oo; z&L-B1SwDRdrZ&6bZ`MbUpT1szS==d>KWD29Fh1W9)=og!IQ&|diB=>BwC?4*NZWY4 zb#|>j$!v5A&%hM#TFOjANbyIR5rq}17dLrYJBKkXRYa5bdD;j671920#Oe$zsxD_= zvn`grar2T)|I16E+sW80LY>XNR;8g*_iIvkIVO`#7cI{|tKI`P3j0c-Hy=Asra30+ z9Ykmw#nCgb+?kqH7n#F98CjPq=;i)=K+_Q0Eh|uMpPZO4KHhozRD-lnh)06^@zZbf z&lpXH|48V4TWMlrjYLMPM@Cm@qHo)ClUkFPM$qkU7OvDAg(m9x&zu%i@k4Tw3q zfZBWth7vt(C+$2|BUH6NmE+~$J5>0e(**>8=Y<`vAdW(^gRy3H9?B7xFMB#yO5&IU zkgq$#6$E;n$+4$T3|#wNr8odgzNc?w@fjvXUT$l;1RI=s(Yx!!{*Zx}yC zm%UoL8WN*2nqnB{^;wSQ;OkGk&rNH+f57cQ+hUyZwa!M4Gl8f}ur{rLF;}W%la9bX z7t$knQ~hFP=_IdDvQ5DDbHo&}1!G{)F+WRMb6DF1nwJ8mrC0IZbyWHD***)f!Tu^7 ze-j!b>r;Cd;oJ%6*Y6;a#iTh+B*!XlJkft84(CFU8(gmR zTpn>yrLJK1Pq=#-$&^%GRDTq2p$V|Ae`d=^s5%3g`zW)1u~8U@xo-TT827ad&xEJ+ zL`8(frkxk}1Bv@?RSC!k+HdQ{Hc6^8@3LCFI`0XKNN_HbIu&rkA`)nIS1|Mitg2*~ z)0bYdesiUHo$v?gL3)+9M;yT|J5HkHxy*3!@%}JldDDlcJI3bTF}C`iu~Yl&M|m|U ziUt8Fn=>_Y1|#b)%_N7X@DKVYuKW3QlP- zAdp+wr%BFufP{vP+UI)lmANE(&ob>aJk5dDi~67_V*8RI55XqPVi8SjE25fQpV)$Y z(bEho#1iE!u|#T9&A~0TYV@j%Wx@l9x22+PP9S6-oqYkBZEow%56=qcn&X}QHJic1 zmh!|^tWy!WXCk}3+VNO;F~}0#;JvX~0lSe*?SoNM%sv;|emsQMr$zmX9Nj;N2sAN~ z^Z(>5GhN);jCUN*rhcsQO1P2=O1Sjq9fzH-l+DT^q)d@)K7;D)VUl`yEUD+6mF~=S z8d}O7!lbLs1Lrq9gclkS$+U(2G2cfm-a@#-T$k(gIU0Fy2=a6r1XI>0EbLD>kj|Cd z6YC%0+w=rj+MY|R?O8dkzg~$^{u8~;DQw1Qot&wN^&z&`w!Gj-qc796>2#;HCIia_ z%^O5~K=Q}cKRR=|3y@2*pG<|3XH9-MWX$m5yz4dw*jkyrM^*B?4}#R@ooC664CS z>dablhn{O6Wmh{}ER(dL+1s%aFP@X@l8cgpl8wsU87{EnY>>z(4+Te0ggR>Fwmk9D zF-twQ>(;>5CNXY-cYsKG{Yox(-48R3#pi&-qO8wm(kpKs?~h)4mVyybrIJ^lU7<|O zVT9a9bLc&?57oJI<&5zluOeMNPqR61&8{RkeMiW%M^`*`8LO`eZoTNWYU@#1R54@F zQMnwlu;{bR^xgfd&3Ern4a*Y(XqTxM`%Yq>w6APCLQnKaN7os=%yXgkltLyu_ouob zsh`ygyfQ~=(H@8X?R6jh%+zts?_6ZmH7jZmA?>#h`UpDS-LjRhYxvMHO4}0lV3tHq z|FFNjB=1$rgGm+juyZcpC;E>_idq+%xx^{1Hufgt(GTeN7l}0_BHEr`d7mjmfnL3{ ze-%2)^yGc}wlL~az92mWTKjQ>9p^67@r+V$$HHoNq<#f^s9;3^o7eRux>xns~(mI0kcwHyCwBGRrIkmqVEp&ZEfLAldS#-U=4;rKkWENuOS^BhW|g zZ7zn>(eik|W9tJBU>^dTiahOWozL=56MVFc>sQ19`w`H{lcl=SVb=*4%aTPsjNHSV zwpWd_rjs(`Gxllo$l~YIHpZuz;x4mnc%6Ia^-Uh%4!_hiCL|6GR;dN9CLJySo0Cg) za|C_Md^aOd?!fs4Xw~ta$y0p?0Q%cX|4SrV%ilkcn78_H*=;7;{^dJ{7#9cPCD?25 zrRyE9Ek&P;=2sZMjYYm{Xp=Zh2!Po{)A~o6k4`$qc~rS%cX*~>l_*wgqM)?)#U6vG zuT~5NG*(EoE?xS@CD4oWYVq~>74ib{{8!-|1{6@eAfHE?(4elzOq8${BU)uxG|`&AvRgAMI9;6 zD#HT+KmdyH76|`$Q3PQXUZ7&^z_PZn_f_L)azWoV&PG18%~L zg5CTR#rLWaF^EZ%Zm$8YAz}5QMUJO>82N;uVWX=!{6^G(?B5|_=3&Sqjt z1uQuSKU}roFQ~4yK2J3jbw#ac58d`oKTGs}=#r+!`;dP%O;1X=EG6V*IDpJqfBKd< zSib1j00F%08v>H~1qZFt*|9=x%L%gi@V$3xffB8F-hs&2h<#nu3;AU2<74nHT)RRv z_(5vo(FP*h4N)JrwUmJn98d}8_0|LDlV{Pjuu{m_iS5kj2JpO>HIjc|+<$>l`@Uaf4O%~-icRpDtF{uB3SMQs97^t~Y7dWet7&TA zF9Ko+YE%@zZPv5nrWJ>bW`X8EGLf~1pbgAE(akr$uTV&gUbTJP8I6ReEA+VXkDC@d z3V7j}nC&?U*+dYZ1{Zb+h`hpL$l60m+Y4SF&NW}g_k%Yo&mbpHFFsq`8E;WU0e zH`&_OCdg{t5d4Rw{HNxEiRW7&O)pr$m%UrI{8lRAKZZR<2DrNaI7^mxdkcVBGWr|N z*?Qz}$I>6TXdkWg5w;r(Y6rlR2%^tmX9CvX0uX1}1pORu{|V2{gCQ4pamI!J_3uI7 z{{9Tzj~{T52`T{~PW~+O1-H|mglsc}&X4JhLbs#5( z^G9r%=_Iypvk%v{3#OiY$|@t!XV_;_pu)yVKJnLa|HrrjsQhjQ6sq&{2S6@T{OH5>npPA3Q9SQKwfSB)FDdaDB*yejKA<+8+zHT>Ow6=~*b3zo&mEzJ158l-b-$Q02 zA1B|+Yu3~>2H^KU@Vh_J@sg|qr4eO_?jP5Iw2KkrIz--2GNLF;E5Pf?X3HFNh=}uF z5ZE91w}IRDMREAYNj?cU{Dc|3Gi*OEEYf&>YrYP^fXVcI;69B$?LOUzoYOCNe*F}3 z>bH3eI{f220A+Ut&e{-*Auug!`JS}kUkLXDX@CwUyv4m+Lwos?_=p_-{fB;AE9S>>{kJ-Tf$#Tmod0ng zfz1UF&6R!uj75~ct(aXnjFlAtaj*U?Qkwg=udng{Fpx+7Ud;M)_vGp`+5+{vn*iSX zpPKyNDu92TjKRo>{6w^@nU*7m>@9q?&=l;0l|8>5O zeH*AZvj58uAO1F7*XHd0hq4c=KVIp7nN}t^M0J;P?fbtBS$;)CQEDQ;_Zd#qP>u$n-I%QnOOZa-ZZEGj>-b>6~R%~`|`WxU-a5(jij>|7* zNBG_CzkkVd@gvoq8W)e1l1J|rUDVGO9~9snA&C+m15qlh>E%<4WzYrRz|w`KY~zcc z1XE26Ki;i2Wg;cr`&a_=2ur@rZL9Yal%MAHjRCc=sU zIlF#A?;ZlebK{{3r7XA^Ev{lq%!!nUBJcDaueevBn+cuit18P-7`@=FR6yZ7%{Jo{ zBYUA$!NWN_9(!IeaNByQ_q138x$9Iu8kdvl38~@v(E02=v-|tkabHHy5I8=(UdWL~ zx~!aJGoW?jYcu@Ri1q)q?Axy0`aJZ4oQ|z=y^J1v@*bH1gnPBLgavPsS7Y8MSUS3r@+$Bo0e&VcSC%Lhq-y^ zaSjA6WA+q63A~}QN@}q-9l*^abb+R05x&=|^N(QPqZ8#;SJqXY3bx`>|2;J&+<2F3@ zfOST<6k6UCTRX9qsJo?cWPUm^R(lZx za$)OM{VvM->Eyj-CO@4%HD*%YI+@y);9_7R%xqCZ6$^>72{wvmz1Od6Db3EDGZx*nn=&&Jti0_gGdhb}4 z=-xI@HkIpSh6)c4IjLVJ23LGQ(t}lQ@gGUSoHkmEDiynLm8QINyf8g*p$QLUwI=pX zL*KvGGSZfYLQ9=4bE?-jU}1C@%(O8Vjd)X6ROV_@UFuXO>BU*-`$Ckj^|r5>vuB%W zVG2+=6a3{9YvaAzRykD)xJeUHWZgXK()_X0IR-xNO>-m+^VT8{J05byV zk)}B2vkp9Y=l7VRG6*l6{2?_DJ1cw!Vc?S?+{t@d+g3dJkfIvI&r?D}Q|6ZqGA6{~V7)yRjX3cVvp7T4v$E^3gLx3Te`r>=g1= zIgrw>KIp=cq773Y+%7$;b^ea@B#Kmfk9T0_Zu}s&!2emF@4fEd_P_TM&+&c*tM&n? z{udlhbQLcST5QGJA(zVBEyS6XD$J|acq9o*$Z?4kfcEx8s{(uYs~4MtkcP#6KMRYR zt7#)R>rU@(IgIF{=h#@=nsAydVZ!Guj1WJu3~=?@zp{9sQYAVW1wBS&8kT7fCkxx* z^$)=LW?QM$j`QOQOItgjg3wx95}Y?b7?4?a;aPMO0sR?a9nOVQhs&T?t80^L&;QGu6|4;r_@AK-=rnm$xY5Sof{%xyBhszW}-&l5U z7m@HzOL6|Cq?1!oIM*$%8m9+NJ*pQEOB*?R)tJUheQLi9^arnDQddlR9ow20b~o^R zZ@kXxZBvjbha6sd8eKqDX6!ox1?T{<={B7=Vpn+4y0$^>bn+Z~ zmDldYaOJ#Lj|Hi8(WAA-2^oMM4}My~4nP2`RVr26S61EWE#_$B!G-4u6Q(XZI}e&` zgA^@Hfk?O>l_u>rs^~|J~ zXZEb2+Zydo*?$bLa5PDqG2G@p2h?48A`iXvhi?%K1oN3<5f2OCtZA=n%gA-MkIz~W zi!M!+7!B|&BlhPg{@ti*Sg- zrEGe6lF!(-U3s0-50o=KIcjeqR_%fhZA+eId(1PwDAHs%bbnGJ+TGkOP^rwd+&&nI z?QuZSW1AQ{0QrR}dtd+I5Hll2&Q$La;d==n`X*UK#nWAdnqQ^8+~}RwrI6W+byGPy zVj{%wDTE7Z%mV#k{{RUNu)#k!cS(jfe(f5DD2#1OXk|iKXYMEm%rY)g%PKqL%~h`O znT{Px%>!B9sYRNVYN8Uh3|x5)X6W;;48${rF486-H-c~<&W|f}S$Gtl(crAQ)9?R) zHlMCJgr%Cl>Q63WXX$yl1)xw@mDl1=FVlYrVc6BfmcF-(?v~9A*D?v0<%Y@OT}r3z zdb`Uq!?jH!WkE2o%Dx2PKzaLk!uK8^jsL^rCL!`H|Gi!rDccc!HXq{RsDSG+wHJ&> zSkFbQw6Y56qxYIL=l=eTW9PR4fOY~<9d6IOfJZf?GKRL%4MB-K`8v-O9T$i_!!;j8 zp)o~nrB~B126&dfgR=utU{+~SD`G$%>Wb;uI7FPeFpE)=P+c}R_BPiF*85U&>lO<6 zHH~~ezken_JX^kgvxOB0ctuSgJ*T@fkTmjvwk$1hQu05Cwsz~l^9%T#lh)57^T|z; z;h1p7X33Xq8Iy*X;=cWrJ3A?WYVbxEDLN9VaAaq=cpbnbEw z$l*weV>GLqr0RvI$Y&8|=zWInsVvVBtJR{J4{FRGn8?8zNVb&ta7#DBc9V9YT(9|y zBRIa^lkGr(LnzFJy4U?^>`lcP2QO}H)nr3i3(fd?2iz7gjNn60Yu>!dWBk{}Ap35( z_txeK#6YoO73;eLZB^yYUuU$*-!OMCsh==+iwweO)lH#yWr?R4<%SAz6J%_GtZO}L z_>(yA5Kc>wug$-@TMAyu6uwJ>GO*jFTpR+oe{L?v1N{Wn${|T|?t#sGjQ8jT@`=xn z(vqBrQTl8>2iJ%(GnLV;<+mCkZwm*=c&-T92(!?KxH1wYA7aEEqCWU#voMxCw7dhq zqMOH8@9>hvGugei9pRGion@o81+jVu4=zYgHIZvA88?kDVC+00=SXpN5pxq z(a$N!(KlE6M3)X*z`jZW$KE%Y{tv17`HxMI@^g;vZ5u;Ae zl$BTnTc*B2_I*MFAnzFDNyZF^lk`ZKV@M?V3(Sa05SxfO;_F-Z#Ln)=FirN|)x#n~r_x&TwV!s^ z5~`K!=Ixqqg=8*#_Qraf2#m8HkOYlXySHvqXK(Xu=LVo}klvGX;fU*0{7G_+txO}n zhb%$Nb8Tif-nvgg^mAZ^5`FbRubnQ;=N>3|0G&gg?Smi(-W}+vseQ;h6gLEF6MTty zz3b8NBMQVhWknQrPCDJCMAK$#A6eOOIob=6Ok&S;IDhSo`qib`vcUXC$kob6WWLSF zvH@n>TQ3h})jq!mhI|aM*7Qgn#uB^0W2cY-qS+dsu#ZatS;Xj`8*<~?!^4*cr@U15 zwd9ELvAmv50rpmZ~6a=aSA=I57ew2ZOZd^E){ zdWz%e9fiGN`?1(7O~luLYNcy3RNf}J6C?^Yr!czKw7^Doas$x}s;mLKrzj9@w5LeS zByLY!ll@pzh-k`>m(AC!qaWZv@uyJ<{C)2U2oj@()MN1o>p2RaBw>AZZui){b0cs3?<6;5^6MtK|stm#1@eG(K(-*V!4p$(SL99tFh5i0R7QiR8 zN&3lEfuWnQwryHmqK9M0vZ5k&DKd=~O3h_;%0&W(@?65EA#pL~*n zb&Oai2)DOQ)a>)Zs{>e{;t z$-*$2n*Zl0Gc@k9@iYDuI`lgIi6H*`z5j-9{v2uj0z~}@MT=bf5k&{0G{BbN2NwMw zi22Vu@87_`pI84cXw;u|6N9w>3bz1n@ZDVO$2Iq_2%r$L{X#naIoS(4$PDe~ zS6N$eo=dd_R@`2{vNvE2lmFuo0AysukD)3ptfhdopu$h~Ff0}uWb;?D@y{aaO8rmPpk)aW+`OQwMruT)KD`~Kpm<1;&Y@FnN|&sRSJ?FfMsviWS? zDtOR%i@EKI;f{Jl_0)w21JKb^0Lj4gvk$PIRvydJ!BGvzwlW_CYAi^GI z`YUb!=NBz|G0zzEtS6ywa1FiU?v!k>jA9 zH$vhH1)C+6;ppi$OjrOHU7C@ZBfOLDh$VYBQZTL~=X_HkK!MBP@P9IgKZ@4C`{(LP zRI4MFmz<{teI=5*=sn)K#y|;_{=)Jt{WOFV##DZKGskgW*g48J4a|ZN((!PjrBA0j zQ=^QTBRzI9(;Y*~3f`xV0mcfw3x&`w&{T38DLz`ew$5>cD?L>#(g~@)Yg$rLIE%5t zUGSbB%^JIbtGzp{I2{a>-ybd#&Qk%~-b|%Rl-m&sZArN2;pinBLf!UAe9Nv6%EQI& zOk1K*#7rkcC>@9$QjJYc2h8MY|nyWwYdti+NHw)wnsOgcrK3o$P3-W_EsR1t&sUl&*SXAM`nDJED*rmKMOMT#ar=>B^Ag{}hai`5 zoybzLbpFgf;#2$7Adu2fyn!P=FC)&#w{9=ZRB@}o z=s3?cDeTDTqX-xfhU)v;m_pw{XeX-=0=B**RV2=(#G@1a?u)bxc_$vQIddTayWNTr zlO+gk&&$E?jSVz^2nIdDUTZVEu&}TB5W|Q;kPTkeSS?j`$e`fB@L6z?9~fCriAGxNC^D#Tjz+BFA`bvv*y=WJ}2^)wr#2x`g5@S%%;nriNO( zQJCka8IhZXCEhRj02F##p>Uh~5j3gh_9I-A=eXA$-g1O8ZeXO@1G7|TH8ke#mQ2L$ zZ1I!FqNFMBhrQXg;B?e?t8cq1e=E~hUUFveo6y0DFt)7eLMMOVLjdxRBDcTf9}60f zaDoLC{ok=I<_1l81E6*yj3Se^{2?y5;(ohvwt^#tw@SWbe;fgFnqciJ zCOy5ytVO-_EC9Z}O-HA_UxTxSa;>*<{3Px$|23(b?rN+NCfw5mYEMn!$29O(r6b5b zj5gG!C$f2y!f23ibX~F4MHyNPj5)f;?|d_J>85CY?dImSq#|I#Eub7H%eTCA7nEsp z5{T1XHA}*k%QGgKB|B5|D)XG`_Y6sNH)3#A7NT={Mxm1pwCnZT2F@&%U%kPSLlYI- zH2J8tlh(-{k!~^lXAC2^kf8ZmiD68`y4?F%GW&cawY+*1dNcAaHS4FV$WjH!h_LVa zdxGF=u;=pIoKoy(0R+y}Hei(MSMp>TzWNzutqV0=8W02uj0CQ?a*5B%U`;}7BLBLh zHu|0MeMPN-FA)PVC&Y3Za%!bBt^i$){9>9F)6X}IjOlG%cTD7N31z+S5%C)nVS+H) zVgU0H^KpW#hcCHajI$0nr`2k;c@SwPm%1;`y0(Sp5q`+dO^KXux=f*Qnz;8&MJBKo zGq|xVy*MIxi4~x?Sgl-K8lB)F?X2KhvG2+GC)77HW@c58udDQB%8~YUhrC!lPRC~) zX)DryV4TKsyI^R&=+MkUgF~^y7UqhVOV#3@Syu zmh*m!i#rb-mFtJ|NSwo7=_$*VB`>n&G7i;|oM${sSF0RcwX0O;z-218*+nWPnF>R3 zl=zJzUM;21oBZRK5*dxl!5gJD&_=2?q1pgWG96xa$dFqLx9(N92nWqq;NBefu!`Zs z>K*h~3H$P%>gRI7(|2hLwLxOIWQJKf&MV5q4_!bbO>xz9-{#?g zwMP_oN;3DG`FW#)%{jY8!>mf1UKC+eLu^T-wwW5Y1>$d^w0w4 z#%HfhLRD47bb|h#47;pJoyLud=R{63f<)fz$xJ>f?cmMmwK^Bt!HLSQz%ml#rAoEU zIz_)a?Ga5rkadM~PH|C`cAFhrwSABO?;%@4q|Ccyp-{a?Cmk{Wq4eqy#0}%N zs%UnJb-AJ`Z^`xM_GaY}b_rwtKX|=TBPc^YtbSCW2~3rvrViAnb%SwXE-G$jozRSf z&sIX>V9b2W?$r@PhH*N;CAv25b1(7+F7W1Tcj;vai-Q|IPNvw`{-nR&gSTR$X3gDF zg(sSOPm31nIdN3D6+GNu*bCH$MI80aKT3!!>k!1O4?0bVfL|V@c-Opy!dK zyf&w=`RsI)m4__-C!5fEY}ng}8GSuq0T8E=QlRUVYMbwA(2_fnD}qt*+VZFsZ6_tU zB2E1IJ9P81-GpFk(UC>rH0yMd=mJCZb@%JSOX!CSY&b@>yrIzpxkf?0Nbv^Ww$Z-;=vh7;u-%1N}P~@$sGd`*%P)O4*#<;g%h>i#uP8kq=)_ zj!JMiRnxj3u|{1KHl?pAW6%A($ayIY5TrJe-zz^Ynfl6Yj|1L#le~x>T9j&D7`;$~ ziJifGfH(LC5`qZ8zs&$xG>tT_+~iF{q~zi6pRS~Kfoe&8uHpApJQlOZq(#}sGtX^U z2ih+(_VKr$4oB`Ql31W{>(+_VJUaTmjM?NMrV}H^!2;Te^)NCyOpVHv z3YHOj#vb51bo-Rn9sR@FPX$s^KDRC`Pc}_{Q_8hA*t0jfxW1N7wk|wB<~+aZl931W zh?0v7o+~EUy_R1ZV;1I-f%y;XSP_%-q-Jy1Mev8(nx5D3=Aq8S3O7P1{thFGSfNo@ zZgF?#sO09&=`2dMO`RxVj1m#D@+(ZMZY4%1icGuh;%Z%0_@(+mKqMzI#BD@PWN9vu z#xpe9@%(9_3U%*UbeVV1Iu{iu zj8TOA>Fq*JLq>RRV5+#0D~?WZajV;(#96wB{dxq@P@;hc70S+TmClemT(whj%6(*5 zsk9=y-_Lx%R%|CPi=QDRqEe1k>-6kxxP`F0tJCJ}oHKI^G{C^m!}-3L*{3<5S!!N; zafLl&(eQy@ymy%O>MXO{%gNg)^dvpRBVzSh+CG$+)(zoB{ut}yQmyA#c*y%HQEn*} z+AjK}t-OA@%h+219u>&??N2;AulJQ{CxUXTy5F@>+IWMJ*ZSU~7}?Qpjtsg;rt|7Lw>_KMgnF;z(hhkEP(44&P%(&IGcK;lC`vIvS7Au zPYQh}{p@t>^=r${FO#YGhMQ;C)6Fg(y;}Uab%ZAEk~uX(=&RDIjs1I0+OF-~gdf_K z*H+3!T|kw76Cxf=IHBe4we{@$Vd@=0F^!4RJvuIjR?8DUyn#Jo~{ocQKQMl*0`;D-0Q+w{%nih1@OV^^P>{ zjnb2k&rDn%>p@T{wSq6Veo0z_t{deYxGf%_IWS)57RegGaB0NuV%T^ikhTnmH(uKF z$TI79hQ}}VrHd`))J9Cw>3~ZcQ}P&uqQE=;k}cucn(Lub9O`yf5!_coUmWZ5=Ei;& z?dCL|jXd(TFp#}seXHnTbBMKGgwjEdJGZK`J9*tlto=9x9CyCl;0O^eOs4W@>dZzE zo<2`URFGT+C8BRwS13G!J{~U{y`Fol9Hn@hV-(8IA91KnGO1gFKHxFJ?8*bREbv*@ z!bBx{xvnBNW-H}B=+&kPI^9094!k=RSqnsmssV8bO9rQRg`nI!+{A6aDP@DMDpcAF z(grv;aPmI*&U9yUZ7|+@hUDo&>)nTY52k;O$MEU{06Lr-&NDks->a{T<7^JaX;xC* z_Y_%damSjLr%~xSKKX#9H{?T? z>8Um>>^Zi;tfx|V>uJOUjT9Ac(h~^ z7fy*QVJ{s7uuRpK6X4<(z`i%EEJhNCi?7ufLW&NyOcuVvd&Qzy>%=1r$~>G|qF}Hw z*6>`n<|Ns$)k=kO>Y(pDt`@DzxOB3`HRs{N6_SEOuj32-45v!pKA`9Bb) zcVRT;wAQ1~rlv1UyQ&7tWqA*1We9xzfOeami~v#WA7B`|48Yra*qaUJAmcSIW+M-| zuhoy!k8^j)3E|Q!k>~ADGT5>pI5M@%EQsY|lD2tND zc3(i;*?S;*&H^lD71w@aO8;@4vc2F8}6GNmu* zF86}iGfs+P`GCcNsQZ8mJNRIEtr-L~D$kl3C-3(qD@OpJ(b@l8lbXdLv zH5Lk787GteP7ASS2t*?k3)LjYXB_(wmd)WV8qb2BM~M6MtW9e$Okn%9gaA__p)>N~ zs%RF0jVkdk{f(tM;AgMytG!UXC-<`Fz{i~`kBF72kE;7)FrLypR-r`)tQ2O(-xXIv zQ-+xlVE|K(A|g-Mt;X==6WimHC@|`TR_ds%ZCa(ilfzz+dbv)fXa;0pO5@4=r9p z%R^aPPsg9GgogT7vkaK^>J)W;?eY;PSMB(QdOh8IN?{~(kU3X*GSjLHCnrzZQCBu_ zm3YivCku$jA^Umy9pxqrw1pxoMmsqv&nL=zqol6S+rn-(6vX06ITN>cC$*j?xq?X? z*9xV&!u=Jq9dJ4S6g98~nDHHW+Kv14dgyudEiG)2pq6{9)at&QYjC%M*p}9+I+44( zTGdL^ziU-D<(5PP(H`cTkELM>!JD0emfAAa8@9IP`>Fc+&Mbggy6)E|TeWaJ;Na=QD1F%!M&YL&!(Q@B#K zo%gLG=5Ee(Ve*VZ_UbzPb zj76p|W)PBXfjSJa=eL_|7O9q{D8~K~?>sS&zu&B#7BHY`hnVvEFl?FzMTPlUGDTQUyqyv}m$b$P2@0*CFihU9~Y< ze@CY!L1_OXyU~FT-P*!IiD&HX@T5DPc$W>CJBaJ^?^8<#0?K1I_^qWE=jz;y6V?@K zsb_tMR$7^CT2zD3kA*nQgVb_BURZZN%h_}poU9M1%A}RXoX3tm3WXf6a~{IB>QqM# zB0lB?plxP94|HQ4j6&ZC2;qx(o1YySB9`Ir#{*_AH@|z&|3DE8<3Emd;6I?Sx&Bzl z=MrD$v}wg0V6$j}JD|wf5onMuek$j_o>Mn%XCgRHHboXqBu$!bJ#hDd{6?T#aNw8mOVwGVR~Jng#hc% zBHLv(lzlXo+A+Lx01L}rg$QDq;%p)`!>sM@BYLNZiK#Ef?wA+iFZ2Ot%ZG)PpkP;O z0~+-ek>NC6HBMhNZ^qGk$#Rp9hdZL%0#{2RDZ5gl*U~aZCsp3jPPx!-(BUpAg0~a{ zkddX{ZC3!J!{2IqXr;N7lhxbya9-RoTpwV8OF3YvSOYY1AE6!T(Xy(3HoQ%JvWe=4 zF0d`q&&#DzWKyo_lv(^-Mw!XfVZlBOZ#OQ#u$AjgVNZ-^Fm948Qu2AXm3@O~*AA$I z^A;L{?N&7)oMyS#=*LfKXmPN|A1K4&JzZmea~~KO%Bmcsfp_-tVAYSCuU#h8j@tEB zbqXFgZ{st@9L#tvciImCM)t`ig9MxI?_a#;I=6lPY!Lbhh_cc+`_u}yq;zuLE*0N~LQS&ZlsM*J--ESF;Myl2o>kvfx zv*P=kpc}3gRiMo)K@tpNMb0$C?fXg$1>2Lr#m;(1Fr_TMHyYr4d>#S_U;~Yz7l~kFnDP}Hv(??S@OLiZ4_p!Ikl(wZE zSrW$*`nQ$_nFjQgI?y7(IMllyKnNgA`%$K3H0r~OI%w&8#GhSa`2u2V6xr`*G!xNs zz^#Q+(L;uV8L|(2Mvp7&VYzCK&d~RhLbJxGk1G)jt1h%H03Ej>Ow__%4*^cQ*+=WC zPZfw3hLp5PnGy7TE2}I}WD}wY98>8{XC=~juUvc-v_EQxoNH?a-cLae9JT0f#xQzB zj?k{U!1q!=_~4w(QkXjr;M&1pQx4+RFowAT)$(WnSa6?_9+s(hB}o9hQe9;+H^xgl z+3#wxr*mKPL?eXb<7cFf<;?;UX53Ml|3O>{B!P+Ki*gI!6!;~;WabVa&P8E6h^GPn zCpj(4gS%|5o5)kG``zx9I$HimGH|4<;A<=m@Vt4;ba>f|a67s3ilLKTd;}4i35q<+ z%%$DdH!Uk2v3(r}z%fEc0x6&YQP_{_=|FEL7VN>7OH%B=V_o^;xcM1(Qr@QNSiz1T zg=VJ27P)N+aKyO-rXdNi(NnOI25NO)-ikECXk?LILiH71Uf#eZ2|znA6JFBZ7ns`f z#P#WR0_VToM_8I#-up3gD>{v^Qk@_sQgb28v)@#gahLYoClK8)5jOc_sA{5HBCLLf zHF31Oc_*`&N%|)&NtPIfEEa|hD_Fs{fX96*oPYn?W7h@--TX*mRP!G3s|_0y* z&yCWDt!cP~l38{>E0g{8%EzW;dQpYkV)@~F3NA518)|JU;x3w-nwVPG)FENfgpEZp zEV=8DDQ^>PxE{XU(zP>Eqp~&;ygGo%drjJDfY+1jp%X~<3ivjKaglX&pZm4(+O4jr=3P1 zlmn-lAei=kHq||0NHBC;gkSPo3zcu(UrM27IBCwIH?w9xeB&cx<<_Gp%wnB=J zmd+X+>pZlu{l!H6eGyhK^HWvMRxLmXIs;1U59l-B=Q4wGBca6GOIWe z*E+etmSymQ_R3Ze45+^6>>7^#%%RZ`$*=udBKlJD2u!Z=L|7DcDy%CO<`2Sb0)>Qj z!}(%yzr#w}p1}RL7y_zOuMc`gx8%NG4q>#H!B^6YN$_2f1rMX2VL%cC($vVr9>ZH* zH3guxCx;3d`B~yDEl69EPxTZJ)-$MY?P2a@Bzz{k=Y;2X+MAzKYYT9PJxC=$r)&B0MJe`QDlgR=}Sh_ig^7T^@0bF-ZC%3JDy* zB4G~}g8>L%%_GdlNtrTf-$K4l8D_q<7qxeERdis*+v`bw%L&YGEoof|9N=>AfBsXk~kkRs)IwF1RCvM?`S)=={b zUmHYWhcnNH7lZm?pYKu+?_ajTs-=7?H!&hpwr8aqZwt3>L$gl~~u%v^jWl>_tG zE`m*)r>d?1{=EyB+b<3Qy{c7tKRlqA8*Y{}_l$Djm7h==Xp|0`99M?aTCly$*7iD3 z54AQ@eD#M|^QIJJW)S3(Cqpm!pQc1V5XXQp^tOo>z-BT0d-X!~<;Q>%J!_wJ@0Hs3 zKL?BenE=}11bFCrL z#M1bN`RNI4nhV_f&0A_QY?smVDoqji@k~{dK-o65gXlf9th!uiTO z`#Q2ZNx|5*`|&#EzpN&k~LvcHsZt8=loo9ckg7ekY2| zNCSU=xDEc?5F$gY_g`5gnjazVMv9xSrDjmG(;e=_=f$p7(@~0`_!~*btcV1R#BAqg zYS_j?JgL%(9)Dgt#H@ZYu3>OTmJ9dr9{k2uHd01ZHAB852Gebq*{ zmQVKsjrRZrd9(RWK~9r5K!=*qHi>Z9eCEXqG+{h=7%x<6kX=whWXDSuz}f&Cls|29 zeGI?*RZZv> z6JDV7%1#JGbtnsMZ~T{t=tpi&yHdVy(^)QM?_g68Xt3!cdzNfplZdN9=uGyHb2yq& zXWbg1G8jEh8%XeHy*k5D4le5mv(ph51|D+&pYnpJ#(kVy4?0cGyrc5Z8zYQs;Mj@1_-v)w9KC;LFYy2kng^bP z=5R$=wO~$*3%kqzL1RBr=yyfxYtLN+BwQl{9V0 zBW((7pD5!+hU*j!%~wF~_&4u^-OG{(AWjGYu~!0KtzdOUq)7S6V=>nLzl=41JO_3y zFf+$+NU)?<-+B){`sh9rfqABXp1{Y4q0HP!Oo}hy?)&n(RL(Y(%!idMI{nh?pNWJ( zml;0tW9{jegNjp)CW=#j#+c~y>DvV<$W`a5aJg+rC!(vlsc5Y+YbA()JZDR5G6NX0 zn=E|_M;xEz@clD;jlr032&y5QQ*gdy?U}WHw1#AE9Psit)yDiG!N7NB{pupS%i_7{ zGu2{#c{il6NO&ZB#GbWiO(qA8|xV|!7nsHBT#n zx$8&uYFP*fUh{yRsUX_e<^5Bu*#bU=6f5UVX^r zRl!bT)-mE+=6@K7PrZJj96m2Go4E6yD;GjtBVw+0DtjI%;BCeGYr~0d3kcM_J48wxHJ3?*{2V zp8N{Hd)Mpvx(w%Co9v_4)Rb?+yagzBCs(-yJ0c_O(|twUziZ%*p6JC9W49$pu`}G{ z=|1l7ZU!`LR%=NEZ0n`l+|ka0OwYI~rBvgdVP4a8Qn-|vnZBlR&$kwkK(qx!FHULr zCat=sU?DBdzH3XF*7OI`g?qwK{VLS!e@u0IFw=6>(XW@?-Q!D9CTDvkcncik(**+1 zfSjCA0WG*{5Eq|V0l_bzoofw^M{W19U`Hzq6#Q`urnq<(beVcES}*j~^q%=kfaKY? zlqbJnUjus7h7ie2acPUlq3(7JDgVn$LFkkppS_8ShSr-Q+rsNs1&0Q|!hPhNzZ4UgON!P$oam{t zE_|48;MapV41L=jT7*@k)X;S;j}3D5JrSQ9*p7PWTw!*UFQbqn5MAvaN17-w>sB7F z^?YMGvzu2O{0}G&Tp$@JY2XMs3k(;g5yYLRTy>kepi#NoW^y-2>Q}Gnmh#-uni~kN zf2|E!Oa_v+2+SbY; zB?*8B(@1fYZQR;ta+GqFj7Nq zjML(&>?uqNo&%gwpQ#m{q{kUNC(xQ^br)Y)CNlCsihr_6;KcnMn& znponpCUhbFY+WO&KyKmFZpGEotQ`jR?G(s#{pL{}T96~e> zhxgpWGNUFRp2LmJ9&(jSRD~k}2v4r~rb_-N}0kLq(nDHmsWq0=*VAhe!6LSaDmqLwRYL>%lZKG0Qm zLdrb(a0Z}`<5?`cU(+CCYozLNas z9ml5GAyQ2l*?`vRpmlf3i}AOb1#ehN|2hy?GlD&A?m09JNgF%T^;uF!h&?qvf#=NY z3?S~6Qu{4NRgMCddWP`Cr3bXfwLMi%=yT>J>bL*Ek66Q}t%#?LEBb@-8VFQ1qKLO? zf@^t}{Lbbwe8dwKXEWrly)}ZGT#L>6o%5pJZPMW#1mLE7qmf|1Se7tpN9~+mt7&V( zyI3gq6nkc#V?^Of;EGMIqZ|ukpkhKJo~jGJeH$PrExJA=7J)&e3Y0`(+*zLPMUGS<<0so-N5cDW{>tS}_8=-{ zqM3Bv0-H20D@#|VZluUB;tnr>?ThGg;x>)e-~ZOZsQOaUebR`PuX!12B&bZer|Qdt zYNv45be0rBb5KQ3$!d~|pnov!6 zV3|(sd4Rj^85MSMQ&Z;hZ$P8-iMRWk@Iikwy9l(prF1+HaJv{hXgnIyV^AKLd9`iV zyxB-m;qZAy6ONYs$PoYw_Pg#lbU2Y`XzY~>>e_xJ;NGmfL#pionmo2wyQP#P*0!D} zPe{{S{l;z4)p^C?bv)4A7d1E)ZpWlBAy7};h$;DCJQF=20sDb6Cdjbfh2ULWQ# z%}iYO4PGRe8-o=Q;V)gv;c9PcT63_uRs~`dLb#$ZrnB8i&5oxpFfoh#-8_S0s(8>) zuGP;@;$43uqy_sA8LIJB?_K z#C54}LDxoa((ic5wrj2j&BiuDV%Y=mpGp?sM;5oCNj6&OAbvZg>tPWxMiPAWO)?WA z6UQ?l;SN@?n-WTF3nAk$(U&+N0ob~Dd*z~X>-n2UKQ(p zZ{pRy*d+#6Mzm>J(>E_)y&gf4lUFx91Z}>W!oP|gWNljqof?gd2Ey+j2I^uyT1~9( zAhPkjZBlnEgFRX@GU(|3@*Mgc<{XZDNdZqE7vz2GSAFm>u-qCKwnw_(GCaJr;E7$`_q(j)Rni5$gpQur=qdUlj@?V2~~*yZtPNDJWFaVFv$J2#Kq-L@$v?>o_mx9gM2 zMtOLk+fnYv-FpS>8~PXvLR(P=7ylo7@7dPW)_o5jMG>WmD4;YI0TJoFD=4URk={i_ z2%%|!03j+>5m2ep5h((phYq0#C?K5#5_**qdMF7A|BW8cea>BeAKH218fc5Qy(^nhgWagR zLC#?tQYQe~B7l;-tqHE^S56EhN?)CVke;8MwvfJ3p<$aEU$guHl4l{cUHN+D+`jMu zOV>Ht!S9I+>PA^`@>hV`U3YkX`7F=Vj;4l_f=xvV-mpzWpPdofnE`;PKQ@2Fw%K(5 z&LF@N3-ivn4l1{vHEse11k2}Foz2tXyyNwSnNSm{rMwVm{$0Fq(Gj z7FC2TxcN{p@Gu2(RPF4@u#qIb-hHMpRs55R8DO?}tR8{I3?s|81S#ikU0NNnNj$Zg zt19bJvGLYP-Mm<9WYlnv z)Mu)lV_A1(+i-F8pghhnK-Gv|@;6lYd>}A_KNO*SHv;wJnS{n?mH{T)d&dSC2bc%q zN_u~aLsN77?dUs&#P`+~C*pJT>~8KF;?F)s2Y2B8vd1tK!Bca~+vQp(K|sSE<7k@p<}sw7Zc8ekzN-#kbJsFC6A=p#R%H;YZ*mTE z1*93x~pKReXj z`FNzQsND8fRTiLG0H*5>X%@QN9@zY9SY|LKEWLj*{^Wy`MxbZ@DYzo-2&Sgykk7cC zqb!0OJq6ubB7bW@2dA3-jb!0@5js5Ki~F(nHy_Rxq1k)sdKm+c`sxeG+Bf?NVpnt`=H}+lgzt)P8{VjgsHGI^k&1yk9gVFjvJnid^2?zBK zYR}*56RkEl(Pe-$L2_M+bl7c6ZeuR2a6x{cg3JbcE)3O6pZfM5xUInBa6Pgt_2ID# zpeDT`Kd>LwoT=q+p5m9`r_o%z87xCg#qFVwgz`C2Uyl!3_VsaCjL&KJp*byZmX?#D zb_afoCqcGTM@?Oi;!tjZhB!J^+GGl%qvR)vf+5n+_^pakcbQb96ck&&@ z9S%Co-M3*Jt-;yP?r#u9ZC%{RVhAdNOUEF&M*#TH#MaA;>;?9$ zD-TYBK!E{(Ov5D9yD`OsYy1gZn9F+@(tgyOy(^}io~m%ScxZEjN}&{WE9dd{Y@8v@ zrH+USk4lrQWvTmjAQ5u8kj`zN8BTP9{9Ij#{SMXE9C7<@dv(k`U|(pQxczr06J7|# z!n|f9JFC41&PDcJ(3`YdqP83t(jZ%Lx2zh2}VcdpcnaxXX-TsZWABRGZ(P zw~Pmwu*VuEy==J0sbfe}(H9swJ8vd1^7;gBZm10OV8P_!nM=^1olAp|MZ>r8D;!S^ zmg3#%13)#>a%R=K?Mt?qJ_V6|U5m0#OXj2Za<{BxBAi};;l}`O`rlss&Mnx$v+(O< z7j*P{d{$?P^0%TD{(7!2w)oI@K&|u~Gn$%^dC-g%%@H8qrB?Eh%?|m5+&*_Ipa8H< zWW_e1v1kj-D=@?_aku-Kl5o^^FB! z*RTh&GNrl)y0aiXd_38$__`$Ffnvp}j*cbU7*JxZ@?4ca$KBb&yutOwo%=9I9vb$c zHYHP~Dr5uFa}-B81#~cerx9;M`me4+wqmZ*k@?3{BwumAu+c>W90`v@P$CFjKCm@v)kbO(|z-3zo~RKA7s#OKRRU z+!Oa&d4)lo>D(*KaAA)zX1Eq!0^;CufO*q1 z!pS=%HuN>ECkE2;x{SxCZ67Ee3wS`fcg*4&;Tm;BFm3<)BOLARYJ#lys~WoVvsY02 ze$6nf3RW8Vvh#;L-~b9~BWTm(eK@c4+EDFq?OvM0I-GuRBIsN}@^IEN>|}IM z6M)>QtEcK@bS4Sr9RHhZ_@#mOlYJvOofBQZr6TezmC|RoKbwh)FFDv>MaBPdA6JkA zyf12GPq5UgErrtqZE2@H98wSCY{rV)-Z0>j9@dj&GHD4&dQ=(ZVjZI#Mkg4)TV9#`)Gq)to08pf?fMz#Tk zui?&b4a5KpSyD79m*b=3&1m5&Mk!gzra!K+pq&a4$DZ52e_`12hu|zU9^X;;S*~?L zchFA2fLCw1!#!(M+Gn+H^R7jI`)bX{E<54ERAe_|WBp16qNMP3+d^-FT8oOOwZLlT z>;38&AL{`AeC;m%CH0|g4V#X?IhASF<{`|xxMep{Q84kZ$+M8 zU7n?k;Gmq5_xKr{kwV0$*GmaIpIfG7E5eq%I)d5u3mH)VysSDxm+yR$5d)@IRa3;8iIWFgNXCJ4E@OFY*{S)R^v z-k~^j?%9maa620doh6$;s@8pbp${W>sq)aS0Hj&zK_tZjvZ`5y5t^ zMYSv17L;i-5Nsw@Vcew0tdn`tRxmI!rl~T%WV*nuKBlWXn^PkJe=Wyve^SioR0Jp_ z+_6jFnt!CiE8=r^AyEuewwVKUiHQKXkr%7a6=7RBSNhzVw0JIvv^Q3=}!LXsLOFpZ_3v}<)+B7u};{5MJEK!QUYJfh{HrA3wjD-mNN7RK$ct*Rr;b4 z2UnF|b&()7b1Po%B!AmZ%>#tlw?F#Lj{#E;u3pcQPB0~8e`kO z^><7FhxuHH?B{L!<_wd!bL}rxC?G%AkY}yinP5=!mHoTUc|j=qMi*0WmLy~G_!3|Q z@HBoe_GR(L#o=gRGEJf2)`vNEe0R&sdQrOk)0CLwUUy>(95#p#RN6{Fiv_T?HaOm` zZnv1PJ9}F!u0LSdY~Dyi>DjDRU5}N7RX5clIgOjOOTh?ZFsiw*XtW6XfSkf8E9&B5 zTjfGED(!{F)`qlBWXJ}?|JsFMPm`sChIsblVR~n5?L2^yJ|W7*N6$#Rech;sM*-gW z9e8`P3dzHnJAz&N^}7tVo3F(K-_yu5)n48uXUC+9W$22XI%mtLs?F+r`Pl6zOWX6^ zC{k&gxz-0-(E0@wobqZY$6O^JGP!yfPhFsEVZHKi6&-q-%mPO%W52;{Ziy5epQzs2 zt{tWHU3go>QtS2|$R`wvFLqD_toeKLV3!(7?frW`0Wueoz%Lbq&@qBf&rzaLj|GlBg_pq`3r&dzr&{07n@F9OXfwL>l@ML$8C29w{=DZu6~F8o zr-(^tOWlZvnYk@KP^pL}=ve}sUG3w|?eqHe1s?lStC`Or9e1hcS}dgc_LN#EAGTaIo=Dhtr28Y|5z|>xhtJb7igG_&5unbJ^P=h<5?gYmVnt~ejYty3bDMA7OEorY#9Wg z)oHi)9ShIjB$CG|Jz9y+?V2S2h-di2Kycp)!&6MIEGqv~vHqiQEdc|Wyq2Za;8(+j z2!nz~{fc$Op9BF#PjL%^2AcB2Yx3&cSdtfTQ9HvV1!&#<3zy1#~awR#Whz zvj7BDcYDFF6G|azs@9?Nr;>kw}+I6JORR8O=f8O~ZO1 zb@W>Ofk+zoXl==$`gzPL(BXgIK&QS=AKNToJ*5DtXW4`cV2Egq%MmXgA z8mRmqCzuV*7iW9r+EG=l6VJKJRfp?PR*zGr8INk^Z}vnX=Af~*Lh8{EuLw{o9gj8~ zN6-i{ITLXghibk0y=JOZlw2d&;irWU_ID11SIKiKv3MSU4g&V@!m;IUZcYFobG%kM zqVf3X5NiZe+u{jiPvU05iT`VnBqV`S>LJa||FwS){0@oEF0lSB#r+p%Egq-HgJp`bA^cpDmG3ie~{9g%yW(&a12g8J@AgE6Qb$`bFn*Wj9NK^vKD)V&) zhus6<=Io)T+ZPDvYbxBP|FfSK_unnX;dBTxgp)`6V4p>f&_z4=Jc6ccx347&xOo4t z6LelX7tW~b%_AHGjylnw}Z_sai-ZuFmj158IEjlW$A;S|(J-Z6tt z@2@-E(KwdFeOMIBk;zd3nBa$c`P)M?BI4YQEwZUk0beMz?@MZ**lfDVmyb-nJh+&Pvr2K^{n+=_27DuP4crL$jMq{ zp>K1LQw{*O81}&r+s(u@&Qtr9H#mf^Nxa(rdl%*(Xo&#Hd`{?l4xbr!kDhdmj!=UL z8I_AcRdL-K4p0r87@!@9bUSqn zUXOib5+-k$aB4DV+S<-P-GY`4;MsCrTV1l%;GEZfp$Drs>-iV9;vb3Wd1qU2LwKO# zg$z(+oGp)fJAe_=Vu0a`JzC3XzjI`=*@wN0F?*}g@^pi9K5>Wr5(0V<0JN^2|M`aZ zrE}41hw61o@*I}F1M7t`VPl)NQcr+x;aDRoFXZ?^mwJX%K=oKyqV{{=t^ z5X~p3V;-jTPHIemRy01?thidahjO@g-SyZA>5j>Q`-h#kl$%TO(r=uEwUWYjI9F8` zk1SFx9$yUkkY~hhxHNQdMDK!SSi^Ru4bE=1lXfhfn&H;12dNSw-ZixkhiF7{v}KMT zeeyrCEJM2#CVip5F0^dT*yr>Z(GvpoV-Yu8hC(yG8;@E0_t5Mm8q>w0pnI1#4sJ7K zHJuu_s~z_qU#Dbnr75RbrTOPP9c%?2#Rjmj_M^DwNO>IwdGE^oL2Gi1&jAzDJB9<7 z`u&*ZN4udD5-(|;ECHERGd-1z=qIGCKfzq_k(qtODa{guxKfE!9Vwbs>?NPUL0#f;tuobL!i%R_vs_|_5hfJmi zx{7t7tExw~zw$m(umWDlcyTOsckLh3qGc}ww$XI1!uCU$3=86+Ap zh3v?Aqt@%E$r?&D*~;FAuhY=t1cB$ov$W2UacYq?YPiV|Zk`a%bIYW)46QX$zb}&j zl6rQRIRM@kp(yY1MV|*@H}iF3{cA>m#!^m3q|o`3ZP3W{jAXle5en;9&U>z{wKK>) zVBqqP=XiJ&tEPR2^g2M3$BCSV4n(#)EL`~)GV3QblWts@i;Ku`y>T^TWlx?4Hn#h% z{^ItqppFTShm#^T+RJR&9lHlGL+xZAp$B=bUbkFDSn6}GSV(A&7cflEyGECfD)spk z)+dGb_5a2%J$t{L!-8|X&#QsHKqXs(E`9-)8Fs}zKn8X*8L|?zAycS0q;+Dm#JFkg zO9qML2CZ^`^5f#DAO~}GaF_i2O8^bWg4`8(xfOP8KLsNaaM}Vk{FjKpZ#U(I}Go$l6Ie(}~;5^|B0FFxw zlaa3mxd&eU9b5Ge)+X)tjW_4y@^Al9?`@uG-_oOJ3r^ec)#nCc^6+0xrlWz#V3})u zfzN1`Yu92=eGe@4ezvM~)U3x}D?jU&rFbDzaQU|hV2yi|d>hSY>`w$;0<*P0%#ELL zXUhs}%pld!K4PRjjxXnl{Dcbs+>Fl~@)8!vNCmVd+}+$Y=Qp=MvvH;=AEw zbP4oc-DXndjf9GjHeA)?;0hhj+~a}gG)B?#k$dJ%cEYq*3g5-l6n)n*O{$!mL@vLN zF>pTYkY!g{%71_2n0JWu(T+$cfYqs626?Q)Mwq)8&BgK!t!kc<)IU5mtksDr=jI$h z*uY8;F4%%vincvXQT#(zoZsHOn?96IH}=wJl_`u(Rj@iX2OZ zj^#9N*Wfatd#6lm{W8i{JnY%+eXc8V9AbX%%EWRG2_lw^I;MC9AN^`#8D-%J?alQ- ze=Cok_P2t%mfjEtoV;7TqG;Svf^X!ZxM18#?e}MkJk8kh`RZd+7Znqh<(#dBrt82V z+NOe>+kR$qzO%w#@C;)o%vr2(HmNw7Z|xIU5tp%t*;RLf^9Bo9BPyB$bCmkl*RT_? zL&=%-vF$tACsm?In`6z0KlL?lF4s@ULnohXQAEbiqR(Yeiaa~N;Oy7uY^+aIe}X>V zfQuiBvj<`>SI=pp+llz{PL-^arH{BGFkEmAT69oVP;&drtFFI84>Kc3kh(m8FQ2#l zJvY{;5kc|$RqxO~i4(iWf-hqi3q#f1kbcsA30)u3u4XKIzY8_PIq47} z9|#B=rQUYx|5xA#1bvE(OkLY^g_*5d)b!be3=*5|N7-E>+cicXH&>LLmJ#McM+hH( zg9v-{66JEJx873;4$_LiyS`6$YGBu2j6nGoK3f^PvRS}6k4RNg0)P`yab=01)1=0{ zfq8eR9w<$!Xzc(qza~0hh;xV;}2(A73Thi;`pkL=*tB4whDUD8n)W+yqi+$M*}; zI9mr!O#T=)J_as1e;f4DRIwU`(=WY=17D0rToF^OIY+0B4Jd>k;J$yByo?P7T;Z2d?0M+mM}1LssGuQ%)S zouS!MB~;|R(56{&GF-V__!ucNgs2DITBV?Gw-e7*7J2S;{qU)9Dd+JD*ocs#9O${F z@93B+HsV4`{6171>PC3}MzwZ(W;xHdLmD0emm<^nhmG>V01K>z`kKR^3{7dYk_BVYeK6LlrihSzxzA!{A3&WBj-}fZWuvX`htYZi_ERFWh_+nbIe2Vm0M zBYqDY59`F^qa8WR6MzBbAM$~0Er;^7p*aV2jw6{_^wK;)9>#vWY&a0fDwfZA?N4wG zU@+*2zGV}L_HONc*|}FCo8~~`OAXZvN#`)pmb7GfWtfbe8rJA(fS`q0qWcrg`9yf_k=Xs!Z*0ZTs zHqG$J7Dh*W!w-W_%NhYX17wj=m6g4%Gh(*9;F)X7NI7wDZxe;aqH@h(*Gb&80*e4W z=$!msV}4%T;Y5PYpUB)TXZ}vl_A&-@LXq?jxN0BjLdZp;XR0nPO`%M0W>)IUO- zb|@5j_Nuz|t5g?_WTW$@>=A4b9wq$JgY(~i+_D`Vv0*=!M0$x4BE7gO<011)5N1xX z6mU^OGWTV~99&`Zlj@zpn8AIe>MZH6Exr#mRPDYXa4rKcMcr0M+-lww+h6hpFP{3T z4zs?o>mlL{fwY9T7hKvbDqpr(-n<48<51#I(fE6CSHhhvO%_KqKOSPR{9Yt|sJssB z)ooh@I5IB;Sq{cv_y1_dUef(D4;o+)%YfR+h!E5rv4!um<4dJ2T;};;#eItDy#6Mv z1T6(?{AOWQ&DEA)A?KwxXEf92Q~2pdprUos&l#>m$KA> zD#?J?Zy)ZUU|Vj$vYO9nNc1%Yh`F2r6vxc$k;AIOEwUgTXd3LHm@kB!oV7(@=1#Do z8KA#4X6{|Z{5ijGzrP|R&d=mr^)A?djqSprzy71d&QPF-7Ji4;I>h=?RNj6QTs;5j zFL`g-zsY+`shcR|e~a)ZG=Ht`qH0AGdr5&bB?wj%bC(@HiL9w(}WY20;^3Z&5%Pm z@PqkVOD}hevEch5i?<%U`-h?JaY_m04`!+UB#M_@0~%t)v%qMCfco#mRnJ8?Zby*# zg3~vHJ7saKUVrY2+N~RFPt>pe6Uh=U$k@d9t?9co+m|JTIe1RcvMSwDzPvE}Wz$BH zOPQ9Xi`w+iSwJeAm>_z#HPQ%Y;5P_FgVdE56x;ZOFk%$8@zs4?g9ZBu-MO zzV+s=giIVA6?$3+KI(EIctWZZ+2)HB^j7hN49b*9pAIVNT`xFP{-}rC4g0_uOdkP?LkNCP{6xRG$cM&jn$*B|bPBmvqjk(f0F2FPaAXqv@#-sucmd=vFqv;0oTc}pk!UxUxKqd%TJdLmwwtO(C2 zgWx8<<5wroi9_;Hu<1ag>|>d}GRX~DJT$OXgi%8vuKWCQ{nWwxvd*0jHRib~sb$JK zKou20mKg>aXqcGGTh$caIMkioonC?6Z$kfsj`STIdwvL7_wcWcAPD4Z-!o*L#?&IA zH-p}dX9N%es#hZ5-FzP;RDq<@>36oSRLC<2>1+R`RqzBx3!0v~DHNE>4jlaR|E2f= z>7F3wvc=mVE=As!CI8nJqaL7%MDvmHJbtRE&o1~Bntdm z?2nw7Von9juplD{k(ROBDpO|}p|l{0RJ#jU;IL|9SUD@WXq=B92g>=`>;BW3{C)DQ zNV)vy_W;;A>PY|jwXG~MkB#&5xt#a@dJcbc4xJZoZ=0ACI~$5Tzv}(OBmRA85g7b` z!z=HUzQKL|X>#5D6Ge*oN^@VLW|L@JKmYT405%=47u1CPz+DUwA)5K8!0ch06W!+7 z>O6ma*fy-PD;YBhyjZgU(;5}OtM|0Y;o#`y_VZY?vMOR^SPa(bj*_*IXg+8Ae^$tr z>=f+EpJqWu-T-7B;zjhFo}DMN40dv(y6-E*0c$fL^Te(f{qZ{iH{t#BNM`-S-~OFn z-h{hQZ4QEdP#!QZ4+HM5kYj{iBrsW(yfEY*2q@(0m-m!^48Lt|4taoZ%u_?YGz&d} z)cc=Hl!|}LEAO0sZ4&@(gHOswhJ-6N`YADQE~MMNvd&lDkgc0O+%);J z<6cXbpB?bIAA($%&jUL2YQg{6wIDlZU+=XQoR)_IBI1x^`0=P0Lh^4#RCYz}W=`^V zEV#1qxcc?uv>jHpUCHLahTb~~=Ev%lWj-;mB$=_GeefmY>e(NT9sy|bZ~Vs*EAlTl zc&y-PHZSy^4ipe!+tQ0{e5z6e+be3Gv46+3Q4(;EGMA>NX$3sETeb0DwrU>IS*w%S9?|ZejLK1aa$vP6$@Q4a z&Q|D_Ef{J;67QEI$mf}9Y{xVU{hZ~_$nEnV_UuLm6qgr2)T3ROt~fi;ihJyaufUd@ zucxt0rLz8ai>tF!c>F5D{5)oOQ~tT328Oza ziV$b9^0p2}OEZW1&@2T?{kZDqtbM92X-1A9r1bekMyK@sQUHE(N$o$^-K&?4(N zD}-1ZVY3VBhftT>D5U$hNiOI3a2f;I&gG~06b#GaC80pDQ%QI@)^2~a>|6R+cy7=} z_bED12;F~-;5Cnxa+cjXQJ@7NT6&*3t7`kS*NVc$G^%j06O2B_}{S@Mx5;HsF% z4-U;({6EhIsnv7f45ysoz{xFz*Lp%lNR$cKkPe(|!he`6U3Dg9Az{Jvj&`-u4``iy z#mGlKoj0WY_5Q#>cxdST9(iflG-T*Za+SEg39Ww!Q||Hd*?Z~W7@%enn$vR+oQ^h9 zUpxuCUo)cYaMBz9Hk}1{RKUDXnYCrDI){AsoN8C_8Z*91?n+ydR6RQsD<92QBt#1i z*cjSu5LKxpIC27(^|3LlHe3y7jOOCQ-N8kugM}zu(b=?4b>%n@yk9~Z0B$=B0*Bws zl$i!`CU$vZo%w}_s|2ULPGvBB3etI$Z)Yk{rhPKmG_VSoP4vap2*&{=+I7{PArOko zRLr9IH;f&j+>bPryli@ycqD~BVJ(BHz=6+*1q zdPS4(WpegXOVqd68*7v=T zKOHyEInb0iMyzmxru=zB`fD+V*W}&G)v_Vm5BGIGVqUz7kH}=Uo&&xd)qKI76KL!0 z!}j}AC4hSB@S;b9ctuKi(w*!w_g9{x!#YN%9Me}1i%!R<{%tWzrXf?cI#w1vH@8wr z$t|}W#2?BhDBCCBD4EXPP^kXQtML8SEsd`rUEjJ|QVHW%e%ETGZkEM?0JIs(npb!; zAQ_!CkcPB8X~xdBfo-?3#>x){U~jR534DjxrbEy9bA<$+a3(qf%Yz?rOs*IA&+fgt zYH{IaTZLjJg0EPKNk3SbJS_5PE`iYEP8KK?%KYc| zK{@4djs5cQ+(-Q4y}j4%(QT&n)2@S< z_9l>Ujj0Rqi^^9}w7uk4LmG$VvEVX2u7Q{6?WKQT9S4PHRvNlX$AZsTS7~`(4KFJF z3{lN9kO$K2^WGi1J6}7^QCAPvdg1ruFjS<5`XXI0^fv4GMG2Mn!V|FXvlU-Eb*;V? zdG-L3Ang`4pUXe{Cz{)0sn&37V~6<0!;lU@P=LX;;|RMbMqKfxysw#q=!5jq9Ef;$ z5pym?&sRv6`trxJF=9c-Ost6CUFH2^_JLVKgEBz+YhYeb<<=h8DP5rjjjUE&K(3M< zs%HC*zky=xC2y3K=fvd(m!=_$8^wOiu9w$T-2Btnfp7!_q?=1aGi{!0wK}P2f$N_~ z+#e{UGNCdq^vRdrx@8K*)lhUEk9t<@PY?G0>-ScCM@f$pAw1kQT9zW;oV}(sW+3h2 z7dXnwbzGT+5JFNf@dF!g#~<(T{>OItL6emVseqAlgMRr;LtcdnU;ue@+Y4pvQfFtC zRiukY`ngu~nd~H>1WCR^g}w)@Gw4KRrzSJ7oYXahcAhFo!e9gU!vu#Y$1nVHTK{?v zk>2&vltCd?YaZ;DBco58~(C}Leh8kz-TUC91Vdhmu$8xuymX7j_ z$6n|`_2M?`SNqg;v_6J*M*=o={FO%yqQd`Kl517(%Bp&mYSKqo-CrWW0=TA-D zwBOt6?C!`ZRp0p*y`*Bl{p;~~F)&kQIH&sbVh4=h+C2ms-rfD-Qe5SRK6jMvar@Nj zCR$;zsUK=JCi0_m7I#9heK9zkc4qZ)YlTdk+$O``g9JNR$D1$8cu-q8vM`;4u}iER zR)i`MCvdR8K?i5X)+4rXQzf2e;Etu{e?D%8+{_yDqusAjvpsg*Z<*RsEwfA8IgMBJ zraKp3VhZUx1bflCuDf6RXWnL537&|QwZc!F>FtrGD6Pif>bK5S5mQ|r{Ok1d##tPk ztHKj^K6kv=*=tB{1dRy9MZS(pg!ye>kZXY#;GYgiZ7fW%Wxsi9zZxabxTy=DXxjB@ z7K!dqVIL0}o{w{RulaB((;;G1p*5g&^wVlLm)$~qlbwJ{>+@laJpx;j&yI??;0Ywa z)i(wNML$d2vSbi|y~6uID1=Y~0oZOH4cWHkCR<*< zkT51Ud8*28#dtP>{c%nQptWveJ(OKu`@Yx!b|N~+%K@PY;-FRi)m+28n=5+*I~WgsNsg z4g0)t)lB2Sq6g4TI!t7K#-5*lh;e`V@t-SA))WG|ynp`tNXh&awtog#{e%0z9NN#1 z<-kAx+MAyr8$b=H{{9p|>|_0BO#b;fg2?~>bHLjCe{wM|xP@^0SH9s^8EY6vLP9Bv zg_L`g)dMoB^de)5Y<{W?7r9D}~}vDKrE)9WiY zW$grXf4yy1jhpzkI)cRQCNl<1H=37s?)yT!m3F>X&8OrQs6=O0r64Wb8jsIi$N;MX z12$@aS*X1<8(Ld~@9N4<9_tsVoq8?U8S?R46o1F3yHPVG9l}vzBK0N=%DXlYb0<2N zX!Xo@RGV|(qgbu|dgE2#_09D`nbGfa($8BA(pQ zqh)7NPD=J@7uZDv` zJIR}S-SDwMBdmP1PC~aSjRl<8pW0qgPY@!&^Eu)B>m`rAo!Lu0*yA3q^R0kdQ-HHQ zw!E3V>gO;_B+r)h>qv~(C&)vb)U=}vmwdRqM%*92zF78d*VjW1C|D5>hHGfuG~W~V zr)Bo0kDO6;dr8e-Nt!x6!fRqOGAc!T>Bic`v7wgHOQ#>X@0`9;*>SY-Qr`h-K|;!) z!j&KgE5Ijzs7ReBbtWqh4)UUM3w%h%gO>tNZ}%uxf#k>wy5n^-ydn$!TN&I0*PXfr z06%-Mv%*t%xzUeO=@b^ zRKtHjTP0H5F{@H_%nyZLG?%{e4i^FJr4Dm$J>ZEZ&2T|xL0sh*ULuLS7NWAeLvacE z&=l=`86Awcfocrv-`0rqo8d{cZIaA-Q|e6isP0cLP#FBIGwA9ix=%i3T&cQ~DFXXU z8Z7i5R3d!V#UHn^O6QmFeHDQQ0|8Z>0&fwt>WySFw~b(6iQ#wO>#(MHpolZe4l1PT zai(JFz|*QUzE4s{Vv=^h@UL*~RJK9jyUpA_tEMD5%jpZ4A6>AOr#mmdf9Up`TTx}L z)&a8Bs2{^`SVw-n;q&JN;EdE)NLeKzL~Spai*;24`#++fak`sPX{+aP zdjPleA(k;*IT5IYTP!Em-Nxu%u% z2(MZQDrod3b%t$Ts0q7tyG|$);SKh+ffEYhCl&9_UjFA<aftKT`qX9>??{bP&FO2C0n1T zvFo-y5gBeiwh>E~Gf_5b66AKbO$ohBzj5XJ)Y}Ls!8_lMGf&qVrsB@FRui|`-gsg; z%NaGnwP=1mdSkON?&Rk7($*10+jglHye6>^a|#t|3ypa;jMM&5shelzM|T&iIn1QO zD&%^0=dP|#pA^0g^U<;cy;P+9@xhdF7Qt0*#xkiXHxVt~bV;b#2!>@W7z&hh$}orp zJ23)uW`-YLyJq0rb3pEm87+H&N&Y@7`@v8X*c7P=MPsS4LieVnpcLhL1c4mV=pgS)qm`Ova{nmRKIL5 zV!oLuFF>$Lksz!FJv45Lsm~IITe+8;Jx3)6RI}&dno8E`TCl{bChBy5!d-)9uVIS& z&;fXx5>#DR#OYq%bWX>ABWh=Iz&h^$ll`juX>FPIHJCW9=Y{ciX~?b ztvQ3@vMnQyy{*W4qxQYaLLuH~Q8u^C7&0r5(TqEY!#`W3tl(YYzNr;n^_$6L^mSg# z%~p%6(wDB+aIiCnYShv6W4knbVvyc}f?bykI-36x(;RHJbq}_5VM*I`&&DGGuS#Zh03Q)^{U{k6Deeb^ zh85ssS1cz#{~A^+81TN{L21dfj&*QoX2@nV$3wrwJ*`OvQFCWX$ii0oT-u|_A7`}em+4mLz!37MAip%)kR_Zaw< z7H1uGeX(OC0X6$?25KB#f+5Cri-u&{m&M{_i2t%4;tew=gkF_!3 zd`CC(t+d6XUx4ln*G+S-{#fxHC#@jICt+F^1wMm$o`6f0z?7wS`)`-lg^vS{YdNU* z%-EIN!>?1dzcXuTcY1%v_2b8O>b z^xV+v?TZJQb@w_Pls7-D03pEAP6J6|wU^_ND^H}tIq5G!t;BucAO8d2t9Y^9&ySLN2eFMPvu zOIAD8Ru(+ivRe%ric?JFr!7y-_vqjvT8^mAi`Qe^L$*!Y$1A6SQ*MQ-i!Skuz!x{# za%|k|d|c?!1?QMTs^s zw7I580kAdB!OCEm&-!|nOr7KE)7+sa0`us_qWe0FacuKc4_&>R*?ma+U-*@KJ8q^K&)a{1wC z0QdgH*qp^Y*P_|rJ%)*c<7=(L*M_(M%YKZh#;WCMZ%4W`$qI9V0VNCTr4urci z%-5MYQtt4nQJdPSPO(wHsr8^=(RvT1J*`PsU$;=M2(UXm{}C)xJ6YW*?rvam!ZnAr zcH($lh3onF3!N82$=eku{0VF7w8IpqhWlW*jjTkUvcKfDBPJ^Igqxw921y-Cy?&TV z5|>&SsoWf8dP*^qB{-#MdlPEEFJHatmMqDC+~=Y}P>{>tCq_a9Xi5;iS2bq&VSNw! z_z?vJu?qp;JcN3&HdcGDFqgRPH>f^3yn_GuY}mz07tyk++^5%WT)#{u;Bp`{giw+M z!mC^x`}l6_$_qw6n*&?k+vg0~Xj+oh?5?fH7;TQeLYY~0-@MzYstdA~bHK6flC)X~z?aw_ATeO6I3il~{B zDje1gjE>oIGH|XMDm;1rgW|wx{ftH1C#?_3>5>H|eJzgER$TQft z)owUtb44r?s%5>?yfm*b!h@mEOfB9eQl?cX8LaB}hQhH@Wk`k51)aK_2EI&Vt}>zZ zq9Yg|v1uFbX8Txs=x1?GW{pLEw@Nysi}h4)q1vm|8=8ubSNpQAe=%LQD<#-A(R$?E|5h@o} zI}#V3y#h}GGRbXN66NN2#)#%Y+E|3*##gK7Y|1;*{VEKrg)ZhhtRcH$mEy-mR7R}1 z)t$LEpKLzV!Lh6W$si?x(ocIqL>f9cxE=biyNg+7$wB>v?A15rql>6h2G~_LEXSp= z2E;*+XM5$1I*@A$^D)^IIQG|eMCToX_`Sp!>uzIrZ!`(zSG$cYwA{SD#v#ph-#SHw z2qKs|<6{c_>`SNq`mCQsj>Y48Z;(Pm6Q-u8g~>%6PFTAmiGLI zY(og;js8exM@%XxtbmY61S8pJUGoL;8@xi9k?vgwmad9v)?3RPi4N%*TTHx`R>2Ib zVqZqm-weLWYhFhXw~lK2Opw@!S@Lhh`;3NPi1~jA(-P8^z5?eRC7Qts)^6w9=MmYP z@=Las?-%gm`?3n4d2!EPv=#Wu*6v$n0es|vr<)2sLOqP{QwLF3Qs3dTqt2q1HHghv ze=B+QIv~q-jY`T&i(0|BcIupOq={=)i|f>c1*eA4eKs3rvrn%o=zC*M9CWyJ?mv#r@Y!8JD15g>UQoHc{e zqmBwZN@X3e%dfO>chp|QcX+H+-zrexC~KtteDFG3TEdkz3o(7+g&IuV%3$2PlX*9H zj*ECVD*KsIKIg-jFzL=(caqf3=YQ0ypIr5*|8n~(AyJktP;>7>=%(I#5oWq5MS@WQ zqUA~yV}Mf=ll$02^-QYCWk!F43Ih361s^vb-}1XQDsvB4`<&}goy`OR=KD>7yf68B z7Y84O4RujI;kaTI3%)pbd{YZd{C%xGEsQ=M`9EMpml$I~t*-AzH zY{H!WEh%PqW`B`nnz>jBE6PUP>g~Ie(sr1FbH;ngu=Sxq3SiAwtsllcla0_RHd4Bq z$7)5q1!=L1%zo>Z_?hwJ^ONP{!yijL!a*@@co(mlvDW=AK2B_7{MG8~$f0|(SFny9 zib*p!53*e!F|R0;5x$2aZyhJ+$Yb~hrS>g}raSDX$ZE0S*;^`K_`3@>&Zv$z`xAs} zJBG3xSj`JkeG~EA<~zoo&7>v%t#{5@gfk^U8<@DI#5W49*yFBuV7XhqOlCy6*&nk7 zWp{ZJMYc_rP0cZLvypY^ZdKe5X#i&zR~ zV#v(cFX?`@l@cztnt@qUQY-S(y!rKwt5)(X<~-iW6n4Aoq@-&Y0dxCK_3+~{RFehK z0m`{=#)r;`frtGA*hE~q&FsSSEb&e-uHoE`zHKBk`G?-bbx#{V^-nYJzSBOauKUpw z9DCjY`Q(hodQ`O*;BF*vfbhC zle}B@u=xmuz1$|dPp#(1S_+y^Z~mavBn#Ys_9#6|T-T6H2T0DOm`hrdC2YEh*;hg` zYkCvjG*iAIc*W;Bw&q5##jDGQ}5=Je%0E_c|tZl<0s{!c)rtikFyTtQFuMT zMfQ}4Ieu=R?dG?0bMjbgS8-<2W^);6qrK6qLAw3yaQKa`o!xUg(>j^dKh+ElMOL@m z+}TwP4G!?elgwQ$W{qP-+gB45&P*`PBWYC+c6@7av$v6sf*t1c#t3^194@}!x(vHR zyR>hGuN;jN(mQO6lLwkp^Dz3+Whzt}^YY#`Zu9ZYdUA7SQ)`V<++(PqZe2r*aDkn~ zb2V5M_@W|5kf`;DWKM|*EaIZm(|-`Ze^z_*Y-IbKcrE0;$l5dJcyYTZ$2X3=oW4hm z=SlUYv5ji9&Jd0!*U_ARDh7&cU8-*K-Y9GdK_rgF{qtQ90{^B%Hi@z%A@k8UMV29L%EWdcNx)Pm zAehJeq469+yHtSkY_GKDE`@D2v?$p8aj?QaoPACu(A4hfYTGMa!Xu0d;)rX%j%vJn zhw#D;RClPaB44*=z_FQDL0#u+oxzbDac(KcF{Xsvo!fQ1&w9J&0r+kB!nIp8VS%e- zDXTi+kE$7ig0LpEYMA5VC7BAglmL5@Hdvat5F=PxUSs@|sLmOevYdld*K1;*=*gwm z+E!i!^TIwF6lquvrwf|Fp#Z~`Lx}v*bPs6XhZ0evtug!9WX;j_8vmJj#zR2 z&~~@F)^donM`dI#D0zsw^C<~<=3la@ z1nB%Q*W~&$UgyQH5}Lxb#Yhs6_grS+SLYp?&5CSINedr|eAk6sJZRb-M%hC?KnK3+ zA0C^e6|E0lGqTXt-rt&+%$?WUcz*{2Ul$R1BR1rCfi8+@DLKrQXQa?c#>;3HzT+pCfB;oxM&Gvv&)RG=T&*>GJQpS3S@8$DwhuX^&1D&A|GqRjf|ZAlkw!R%cTnx-!FpeBu03|m>CCM_KGsFWv}rpy91rHP=S zMh^{()iZK`MtN&Y};sNd9}yF3_n0^>c2)Kyo^jWQ9I8e0<8tL`q9Edl6+v-)&Ir)rFAV{(+>!x@ zF18lk^SM4^en!l7QRb~z@L9yDQSVDr>vNsOeTewuk;AbT@S}#k!?K_r8E7T`NASMeaWwf0m*a_s`MGmosfwzD3IJKSbXV zz)z0-dynv5)XdwoZPA{sU4&obT12QMz0nHjrud5$#86vU&$CuwN)w%>3bW9CL8w@n zhzRn*2mR-}WGA{DybsCBc)vs3qd;-VELqg5$SA^JCf zd@{voz0i`teAK1S_sYJxP4}Sq!H!4Nb&!9spf}e|j|Ag%+X)V8;Wln?udyZQDXiE6 znpP$PSbC}mo-g}eKduhAFD(rkhqqu;(KHU0pJ!lHNsJ8b>bE_3CAD4@D}K3_Aa4=Q z{|oslRxi4?<74gG(T z=d&ucN1pul)&lqo7#<{RzTxq1yqdudWc$NwVs2|(6Y-+%;M=JC{)b&?K0lvZ7t+eE zEUKp7ErsM!P60-R-D$BvdJ2wi4hp7snfT;rAxMS(<1BwDIjJ~ymg8tCqo^Wh3*PPc zc!!i8oEalN?!Li5H7P-JceE&_bqP}O5_}fJm*3P}cugr^7h`XAKvV90}VF==zwAC2X}?uvX%}kiY^Rsv`(e0*l95fHgq_$XUHL zw;AcrL2>XqaNfGMw|`}?5a!kL{ceg#d-V`$3?Cn|JO76b=Gj+&6{g$FtTL`vA_s5Y zhd5}>G_X7&xPwA0LHenNwFe>h6J*InE0?T7kV+Zx&}~i8hZ4sNAZ(kMFTEOx=gHxy z=fLMFzh0Z17hG(lsN4v0YEq8gN|k7ndtgN_QqMfR((<@75)~=B_SmYfm#p`v4YpOe zP$&)X+u3zMb5yFNtlDD!rnDW2x$<%AF3YW(`D%eE<`)Ibq&E^?Cx4NN*N2t^P0w%j zd|iiIA6wAptJ-Ct9-7urmz|{cz3?D)^%P|6eo5!x)s>(-b7~y@HB9~BMM&?0&(|w) z`a1KqjL!r$-_#|gNZy+1hT>8!d&%K+P>XqaXng=qNR)&N=#x@TaD0Q zbtvU^_c8hj#yrk3SUPtds;`Qxe(oA5cI{x^tfM`3d&53djnz}-!gb!vErb26tujy# z|B(I8#FlncZO2RSOsOTZi6nw@X2Tf4EC?&t@RpAwQ0X!LWRXK%;a4%)Ik<^-&04kD#ws6Z-WM_g%?S` zo_Cl#CHscDw$%1Mi!4ZUc-Znp^L|WJZJ6h(qZ-Nh?TrpM34%}|`V5u&q zlA=4fCdTV=^UT(DCQkMi?F}z-Tb!mptI{?`a@uE$Wz@Q8q)=wIJESSEouV#%!6MXW zTls-%j>IF_HTvy{77vs{vQv(M>X1Vk9r#UXL~e@vT5PuyEK2%;RcASCqiSgu_QD2# zTn&^cst&Aej6`?$L{bT4!V}gEPkdC%n~;R zHtJM&{w%rGf1zflcZts8=Qws7oupx}M+pmfCI;z3dh;@^!}}pcg)_lln{Ljf^(`2} z;KeFG?h)3>N4_EHTc@I%Y>)HC|H<|K@URZ3Ob!{yJ-Z-$Dt&wC)pmnCuZn@LeWe&?bNzGI551YNvS)TGqmnXg&9(RwH zlMje6t@mNoQeOjQ%$I5aJEx-dE&cZPBln~pngo1R%O;m-u?X)~Tzbg1N6v!~A4ESy z$V_lPf#6h79WFJ)MZKLakkBn~AZM(=o3@tB`}mY$!E>38aB)z8`>B)P`hUTW4m2uHlm1rgIXdI;~iXvyO6X?8GG_Y^wK>nkS_v$H;?9+QP1q3!XmdM2#JhMK+yMHG5I`QGYk_|ymZsWU*2=#c;D4zxUTWO`eV#2d@ zB<=nm>99zXU_Of5@M~o5MQE`D<7(c%Q7W#y#eTPWaRaSh6?JQX`owpl$X(jz0kfF8 zS52>zxE%=0i;0-dah&4ZH<5R_BP0Dr&O9L0(n!*T%-Wa?^5ks=KgM6Y>Ou*;2u=kd}F}(I&a>tg5?ua zVfe+Zb(r3XxkS&5E8jBIg~Ff{*N{_+`Rt!16IdJQ3XBjVv?5!g zYk}O06#Sx?=Hmq8hlsmI%`;mvwyvU6#@#n`2Fw>I!SB{*%@e5^Z>s*P!fv76uJ18B zEPb!CbDJQ0^TZO`huW?h!(Ol{!}-rj78DM@4d{bwuCJ*~M_O3bw)NRp4-^+4<~VyO zLlzQ6EwG9c>%r3DHW#@4%0NyJKwCYC6SnwfDv)~~wdBJzKr`@hPWmZs7KrQj2Hfo5 z*s_7_fMEE{>p1z^78Kk=@dM|3O!4&~5ZgFY8_e5K)I3q|hj;jWX*w=|!uw_L#-7gS z$3r&08^+}S%j3#IfBipLEsua-=je6+5@r$eowVN6Zl?IcQDLsX9F=35=uO|qw@y29#yIE?Acv+rw2P@uu!9`p*>;o5;uRX zT#z+543FI}0@7V*PRby%anhTsnI@0dn$Hm*QVp5tU0@_TP(xu}7D&d*bj!oN6_~># z{Ee2~HLWI~Vi=t&2FtW-v0qZwT+}MG8fQU`-buRop;c5eitFWeprPNvGokEfyzp|4 zaBG!fzKEr=^VzS|TqFbcA5lzJ9Hw(?n-cjwaI=sPN=-s*5#gG&jX=yr9kd+^SNan9 z#2!oMg-IE-*r@Q|HhbqzFux$%Atc*5#@Wh)ovjUcCo+oT?TS>coQvW$BkGZ3Qr|@S*dF~@olJlKk zMmQquff=~zz zE#->+k~IOyyOW%Z*8OS6lgL&mI$o3o8D@Q9@hmBs+ln5?dpob472zgnPJZ3DpZQWo ztwaa9Y~WBhYbSKN)48lOTp-~&&b7jG9`39=* zJvmpYq-nDnXN-s3_`H%r3@5sDXE^^se5!eN5LSvRSgivGSQJitE|A!6yD`$uqE85^PZ~q0IG?Fk`zlh!`CY3gRt_ zGnP@WQ<7DB-skP^DwZ$gKf`R(7y+*(TPHl5e_XlZX7CeCmvG%42oUDXo>ggAN*+7I zoCO&VK}+@=di18pl7oEywY*Ziy9alllef<<13pPTGtwAFQ>4X}RdMJLMqN10UE@!T z)ym7CEAv3ODB3Qi;WY+(_0uhukdgX2w~G5%l1HQOv!Q?k{Ve&6OOjIQc(*dlgGeDDsWWBZo&#eZKcnxyEpe zBPb4Bg6v@{a+q4sJ0w+%H+^@R6<%^rOSc|kW7^D)AZO~OLt_OQu|yB0 zaiUFqQ)<;%x4=4}&VtLgB)j(d#FBwRkr3ewci;U9 zAEuI?n4ZxLU*Ue@*Rh%H=-%p)76wv#`#1gXYH=yeKYAf&OJZF9f9t9Ymu*B5@_5@b z@*7lNA|pC1vSA0G_rV^dm}1R9t@~R4t}T65fs&{tFh2^5dfiIrafkjoiE5~YwE$xm zUBhK2+hw?t-mXlY@gH4<{PwW+*B@VVW|2jbI-Gc?kEA}q?g-~iVXvf-gX14_(Z10* zCG+P+kX7=g_BOJsOh%OZPd#lEOORYOW<|H&oxbx4lnL@$8mX`Z z&QNcsmrXq(XSZ%TkJeFM{m}UUlIA*b_uOj(@S${7zR%rj|8A1ipeQH)!!MX4UZzt( z8+mw+F7=O0in^<#03Sli)2sVFpBHvJSbYW3xk(RSTEp}%WgDcLK;QoLut=RSfm-j! z$*Ae~@3x>HLIw@n;4JYBN5SN2(<}dK3@-x;tW<;QFgd zADZkM=lE}Yhxw1aI`1I+x(-XbFO>0;x$$>Xol;1Lh=O|tHd{E>F5Va8F;+L)XU6jb z&DDQH*T!o*7#~T(A56Te;SGi4?jZ^gg5<0E$-Rk)ui!&Zs!J2>^;7gY2arvmp^Ks* z-i2+;ZCN@Lv1+2DNJ-s7hD{ZhxW(}AO!6HyWE9_;m)2IyU6!+&J;DeKb&PLRZg0GP zVvOY}-5gY;FWVa;GWwL}{D!%5sJ5(Yu!4}uuPdl(CsrUUG6)v#z$3pj#LuD*@q&xi zac__6vxVndEmRH_wqTN)Yd#%!Ca@wx@VyKy%b8n+e&T}scWOV%%XA1$4yB(OKU8&z3D|&)V`}3Nwuk5JF`Int z`FILl49uyKV={%qcZ7J*nMW8YI-Us?L2>JI;FwN?L-w9`nma6|wUL-A>M6Wuy)1~F z#Vz+(^_*=5Uo<6jez5GbQmw@<+Lw?SCG|4N|HE(9%7sIX#)glilq1;CbI~UHLJ5 z$zu8yJX+5`B9ZZMjJ^2U$|-NmX&QvsRtuaFez!O)u;5IZ zgo`(mc5{U5NO^8>HRI=#`Z1MnmV(p0owJw4&J$T@Ei$c#u(%r~teQ5X)j*~TAKjD; z41Zry-Eetl8r?raKETyxK2%fS78{+mQHpPp=(Lr+G-n2DUZ9mo@VL-UX`(!X3lZeQ_s z#EK|Sw!j(nGGH@)DY|!_6}2@-j^T1&p>TS!Db&zYI^B_?^`{qxIEEeFh7s7Y7$SCQ zXKj2peS@Fn@=V|a;Qa7i;7o+PP0PGsRyMa;ZuDLpeQDU%wl;@@v?2=4%->J5vYqlj|OjhRoU5GiSqSeO>kY=)13s zU-@;V{9H@cO6?PS;V;r&mFd^BpMEZWY4)Xc5OErQcK646pL`@|fzZtmkj6r*5x6s~ zhBg7VI*L6u$y|%%n?QSUoj;Y+`)UccChO;+0^dyVK5A@cU+d0K{^ItCZ+f2dBb~jb zCe?Eo^N3ybKS;{3nw@}>g;HB(UOlp91%mjgRpxy1GS{A#ok)cs zujsYqxZ#7De8)xukJ>ADR1W(WLF!?D;g|hTGDg|TnL3#&<=`tjrTdycQ&5b+j^=8x z$ksPso^?CYZny&z9}qZv&f*$YHnKCOSzE_Rs*Ug~v4V3BWE_)8)hk^DQ{^xc-C=3^ zE%+Bh(k^;%{+9Tqgh97V@fyafc?`nDSi;rX2XXrBYe^fx3ra$Wk5>6$zo@M>1JcwU zK6uVpw~bq<66~MKDSM}kYkIidX2TTs>nhTkY2Y`KkvzKh*REBTGs#*W#KH|^m~Bt= zI^3aRemC+Kwo4_bkr+)dAkY51UkikhkH$TK{`FcWQ<#Qpi+J_}-pTFcjEGfqPjGAx z@=c;c5>H%}VQpnTpZ(gfOkasfsBr?vA!B@N1HsGMi8Wlx z2^DB!`lx67Fa*NHAMJrxrqT5aO40TtNQf!vwSZ{J@Z&0&1Gag;EU%X>qN1L(2E8s% zn}qIjU{jeAPpfQKg%9TcUFj5E2DylNeC$i;QYo)BnKlgHH&jtLepNmaAyG$=>mbF7 zX(m+bOSx)fSUmpym6P>8-%%sO^B+~dR-ZL%I=#wH-f3jks{MovWEF3~0TIPJzC(<- zq7*DkvcBCq_xFt5Ub8>@h_`D?$Q}&+dD5X=FhCRvC6V!~hZOkU1zog+V zWQ($HRpf-cqLEmKzc_K#gV$KS4c%T-D3c!fu-!$+Q!%esl)W2`A3$9JI-#<}GKQ^h zOA<5f=v|flee{1)11`J0rW2M)0bC>FX6Jl%KLWN;s=ASTssPRb!I7gONBMf@1!g!4 ztTO9n0q6xiPuVU}-7`h(Rjgy!!RU24YQ#GR(56$+P?eSw|G|&KZroZPA?zrssQ^6^ zdy9uv~tEUXl{aruth;{sa?a{Vrr)$k687ZvWnd7MYxK z^4J;@wv={%ESv^Mqv-#8$GQ4iYn27<#(=-4txRKRge`1`_R=i@#85w-ul*m*4&2LE zZIc~pj(DtN)N$**S9mgjj;NG@NVz%KSU=IIvA`5k8~XS3Hp)NQI7a_a{)=D$jUJr; z@8vd%DwtCtlf@59Bw`22Y!>&KP!W-}oUT;twFgNxIn~rz69%J%eG=O=WSNsIC{J-v z&2-s{p!$B_R!?IX(b1>Yj$d*07`265J{RzCpxnz}24r32qMCFz)Lkmtl&R%wF1E=k zghD{zvatuYCjcqY=5=(Ji}M#WQDJz6=+wOFMN*qAaj&w*yH_af_9~^mSFkcThX)?I%QLli_p5XYtJi-UbZ-&RBWHf?p(Go zo@<;pKievqE^}!+xMWY3wue4n!WalRQm1T%Z^(CsJmEkJ&k_W5h*n&DY*D~rcb2ef z9g)A9%{?BFil%&7w>LO`srPriDJCE9K9hd^ZNtrDvI*cu@GV_iH)_g6I9(Z(#IB(1);R%uQPntF=04hDnP81TLDuW6PjJ`+4x{P~WoXr9 zjSbkp4GEoal2paBUy2*rK3@%d_!N(O8tvJ4Q~NI;N#!+t)*qE7zB`Qz6U>cr#JeJ9 zV`T~y+6RTMZ*Oq+RVGCVkvq(D6SSmYSz_vze(VQy|2H>w@CXlC zZHF-8wf)=ZCEMSIMqDn~v?@3NnAca=78Xh}@ad8cYXW61OB6YUQ%(-N5y__f`{U-W zcsi_EHtGCVp zrmOA*5S5Xv<330C@Ef+D{r}niD>^{eJr+So$iZKmG_HTS{c-{WMV$b&zx+EVOFA0fh05}Wwy9D@7D$8Dy z{~O)Q-&^6lv% z@c+E-*)kDw77L@<#(wvji%UT3Gy6!;Zrgj%>DlZBLR@n^=pbT8^FI+Fr|nN5mnZ@# zi$=zlqt5Ayp#72ur$8(Gs;UO?K9Yfp(i1kI#Mm+8?|ZViOv)#%B>E%rMaE zspA~ZX%5{@$KvHVDqa-`?rZ2<%-y%Qfa6gRxu|LvXbJ+5o^P1M$A|ryaO` zS0lt0Z{yA*e6bF~`4C9;7U`G>0$Oxh>A!p*(3%dUP{<`ZWUnrfuy4P9U`lobz-Wbx zNeU+u1LY@d*oLv%gRbgHJ}H$|lV(B^A_%(k&P1Y}H74KEH#xGFWq~%{U**vHV&A&y zz3QOz+X?F^)5h$wuUpg4q%JQwsp%_OiL7l51$69RdB)#Q=c9h{SHk7Ska45%4`btU zTRs(VEAag$02qZH%Eck@_FJ)zbvo{UmBT;;ciy6czObA|2QF5rp36qHR9<@gD{)54 zSG*vbdu;O-U<3Xhr^*sQKKRXyJEDO}WgopJ%r4g@vX-2Y)`feFJ?p>BIB?WWh9EHA zDj@!zdCjM=6V)haId^?p9_VY^RH6>hy<{&c8ZXmzI)QPXG4>J*uc_OP{!EOP`_^s( zJc`TVZ*s+Y_-_)9@#zCp-NsuGN7UTSsRmT%w*kba!c729TWGixudUr(;4kIzjLr2{ zTO9t=>(Dwy0FtwAIr#->vZj^kLqO&OFDBjGy)(`~Kj4$;QEveFH`!@ON8f;k{>WyLxM7 z<6ANVu%8N>V?OUCTQmmPHeW_G2U-Sq%grDai?lCRbIyywTR<^klM^(2&ux>lY*=W6 zpxZ`lsWOgBK`-*ab%?U%>6h7%Cbst3eGftxibAWv3X>BE1$11d;l;eoNx`%<_^j<{ zEx@V{7~9MngHQ-QY@GE`1W3fjBFK`!nGDrrvG1HR3?P&Hu{A6^KN=Cm8sh>ZXK(g0 z7g$&RU@T|00FCBTB;6@67G_-ty>6g_)^|4`sv=&$YOD(^skRNgwz0}9^-Y~)w%^^i zy2)+u>=d+VF}-FCpUkZqDXb|omK$Fsx+91DM`EiQrj~@UhfcjFrmGG*#A)Cb(i-x? zYXoVaY#@6>9Tt86Jv^tQ`H$%>PkP63A?HYOMJq}nk&z#HuIJ1vhR{g9PidG_l$ zW><-|NT%PZA!BB*aD5HS-c$yEkyFm-oBeuI$~KHG=(Gyb^4r6~6QEN)x4c&Ym~=1Q z@fj@0>dzy_HFp@sq8#ansNSuW_vrKwmxqo0@tgWQ^2SB}VvooE?87dKU*9AAj+A_@dW@E6XTQ40PJf}NPYT-|eS$G8hX9c~;FtNqF;bJhE{aiSDAX8~;K#NWJoFjO7NMkuH1g(bAk z(;fq&Ud5=8vzO1eA7at-`7{_vd;L+=5^2;L#aApJ7406`^5|vxsU#O|ba}<^=90Baz!eD|n1<~K z(?-{pC+}eZxja~>F?We&l}A_qUW%41D+Mgh%KpI?Bp1(dKF2|1lqOi` zqt=gntD3vVpFoI~dFk-6fuFX8c~@+JoKz0<~)lg4K2{XYKv74x%m4JhH`W>rd)>#htWUKSA_F!Grw zJ@WY+?e0O`lxkYlI@^8GH^YWAJhOr!Cy6Fb%OM-?+JD zrde+|<^wx2xhUwTgKHf1$2rz0p(zpgbG|9Mchil1PFPtT-R-QN}qB(QLeUUG)eAHPSRwK?VO4#fx6{DY>=iZahw@kysXYz1KLcR4bnw5H zMJ8y${w1j|lebkfnYEXJWm3G@RfG?9E%xVQ^Nq}-qIm7l^^tsGqQMyu0rKPRA<2Y_ zoQu%Y>eU3%+8rO^}a5qFqNu2IbixL2NG`h>3RkC^*Pdr^!N|z=cOI!deI5HcW z`aO<9%2oGt_(>&tyi<70TKTmg6hYAaNj+!u#f|P7bT5x_gW+N^_9bRHDH>&k&!$l$ zbtoYPWjofsHpBvk^Wt0~mQ=+KErHz+2|n!(#>Mk}u%e`ESA4-{I0F&*rFu{VUiS>naPHs%B+3v%YS5wcNpUeV(pn zTQ!0bH1F~pAwAvHpl5{}a##YF#~l0SQg)b)8_l_}(+6XxJI#@~Yw1m^$|Lrx5s5_# z+AKVIa2e6Pszga}ZDQjJ)#N_ad-r)eC+^-ZOf*-rPHujD|0+g)5*>3-=&SX{qlPAx zyLXx|Dca7TvTYXnc=kx^i06-)f^%^x&mnIrDaFnc zMQTHiZ}eSFQ=uX}R3CE7SyKI@+vP{zP=2lQ%F*{lzoYoh+q&7BP!|{Kmm%g5ihf@X z`2Jf>P&Ugo{ggF6e6J~+d6#*)d@H5)V2a|0YouZ`?^Pl8e55))_haK?BWshl>0Bs2 z17!8D0OVWK&zb(GJE!(0xLk?bbD5Rk0n!^7`-q)8O3D4sIctsnHndkqY>O3J7#jJg zkhc4qvOb2 zkZH(J;?04Tx!PU+vwvvA#x(}-ISaDb1)Iq>4F$A9kiMVK>;(MW2qYXw4==cK;~0m# z;j^{G%=u|bu*-P8!UEvNVxe-$j27TZZnqE8g>H%KzF(V*#CX_`589MuXg@q6~Xq&>>^ z&pnIAI1Q$Rdo#Mdz4r$4TTXd1)pON}MT%#u(7RvwUh4`2O+Wj@D0b0~p!(j4d)ef1 zq0-`e|1xwnMJ&nITKaF$acCJvWSbKD4f+;m7te$oWiT_dYe!n8qZUnXU)PUcr>z#6 zSR9qC;m|PG3VJIF`cZ<0Cc_+$o_6M2`X97>ED%1=pqIz zCp5LhJj8R4E6MN8i^m~&8JWUc)?IR$kX+oSsx)ngso@0D+@ z$}?$6P*yCV)fu;pRyJhJ;H9J6r1wK+FJpY@OA+P;(FK0Yy=ccpNOIYVsH=(1Bgk#l zwIfR9{rslzvs5OF5XheY+p1;J$2Pjn?GB$*69H~>*!WxMAB`7&+i}7tWb`uQcUUCn z+_Wl{#w&#^WikL z9-L=wN3!%Uf@CM9X&sDLvkb}z*`SJ4Mo+C*ENvMgbR6^`1=H0QAts>un|d5X0XG#~ zgxd>R`+ZC9*x{$81}n9%q4pWqC(CGFtuLLR6Gvq@6QmFbt(!U;l9=}7yHnSH<>l+Z zXc@YDHCSx62w%}_@IWTU40 zsm2S>)K4@r7O+RO0+edCYFMtq<)TP&AEIt_EeKg+qt4$-3)I>;2N~is1FD>viwa_H zVffsx4c;foD~TNYd*>SUx%DiR_yHwDWBO**vq zmK$wudx~A{9^~$Y<&joQi=L3nRjG_4V@ylVpEe}XzsXBd7=hap7JZrmCzoF%zVKP! zsAxh_&~-dGFbRI&`*Ha)BeCAe9HO=!^yN_DPV=6;!FtYbM<=wzx<+^lm~MJ6qSt5! z&95g#=xEPOm7!8h8s?~^ob;j!o^@@q=_^l~eoj=~fD#L|PD+32^r67WSyyJUWi4m* zZ;Vu7!jEWltS1qs4?f-Oqj#yRZZ4|gXLmDdHs?2;70(a?H86@S@5FO1$l zb{b9jvtnWU=`4Wk^-Hs#aG2xhTM1l9k%Qn}iYk-2y;+~BUH;WyKhYg&CQza$_$q5} zY+dng0V`_;gUeFx!D7XZWld3e``3gj#E;vO;@QPR-OkEE3;$GjJP@u7DXf_wek)=7 zN`0)~1$_-WF5*Bf^;V+m^8PQOu$ute{r)SUK_@esJ{4;&PtqjjQDSD)Pa~)`pZ z^zeui!mXWuq(Y!<)YOh}4q@++B~@0#<%8pz#M(>_{mCIQJJ5%zj|7K!TyrF&Lt_+< zB*7$|2h;J7kHsMiBx>SBj9jm7?;nG8Ym2eMeNGnIt&Vlb+=p(?oe(T1GL(^PU``-= zS@~(O$P`OWCBwehbF9S>7tgVL?`&KzhC#fsD5vF$y+x|5Mon4$#Xr`*9s$V=Ax zEn(u8qiQG3W(~}4Q^)H#v`<#nrtH2SZqd1EEnabP|H?*Y-9I8>B#7Q)F`ym>*xmGl7Z8~r}q=C_f)Z}CqJ2Pf6bn##AzLMi|nwIF!+{8jj zHwZ62LAO#UX4D~=*wfENTl?VKTy3X8kCKp>z>Z!z%#>)x8*Bi$1jrr}P}Qj2$a|#v ztA3!X+NmWfQ=PS<&bT?Ez8N~xx1MyxwndKKs7>DUt%$w8`&BdeilzW#c9dV^v4mzf#3rw+{Q@$CI|PMm3! zfecMWWN30#@~$$FdB6pIKe%=4>f87qHNz75!Z)s7{pX2HF~-XS%syUoU~vEeVe6q* zC$W6b*4dT)EmBZ2wdZw@&~4CtZPQY8R5oU ze?r;Xee)26s|T7@Ta_u4*>l-V5Zr_lZTA0lDuIxGEx)lcttXb}4_ccQ?bCJ8IYni# zD=vAAqePR4<5+i;en!cZ(kjCX_y~mHNh4I%Zc~m7;bvxydR53u+Ss`20}k>@)f{%@H;X z5FGrX<76YHc%&jY#pQ44?Be>Sj+vzzokb&ZN<%~O?cfgxlf6D$C<5$|KH8ug$0wQ< z=?*7ncXn;pUv$8Yng;8RzYAclUN8H@IFS*gkz=QE4NTXu@HR}0^9mLWcSZbm2oH+< z?y|b)Q|_Gw9dx(>J)jUDo8@5JmycK7d2tBjGg3}~!9Bqo`lxT5HYfGQf_+T8#EKA! zr|WrpG4hUR>1yYPtYJiAdfBJ^0r@+yF(O6&9#X!|-pP-6cP1|m0K&Snt6hz!Wzm6O ztnGyCs_j6W*6g+1CvuqTJ^!>0m!?39pQ2;h=s>!kPiZX7|FqPY4ctVa7LT|1~ukS(6Yum$V+Jc^5Mw}z+ezxKXED8LWZps%2&Q{OC zxMI!zf$>Ebz7BKvLJ~reGQggLDdSxayKQpBOt-=TGAOTG&Yjt_Qy~;mOZhW!NtM8_qP6h)blj-MfDtvgNY@SG3m5K zkxNyODzW=A^K*UwX|9>cZMquzs5CaL6Rp#5+uitny%qEsp3`TDwZ z7ziFG+_o4SQP#uyA@>gGPLf8}mS}6JBfDtS6=^DiZpDGP!btJmjyNxQ&(|p~>!IqE zq}T|nu!S8%g2sq0SGpA$AzR>%a`Q-QSaqQtzKY2;ms1762^?pJ2e!I%9aVl5%b-s-< z5hnQ0?U=f$?9{KPE)}bskkXX>G2vB%!yI6m5b>gps4l6G3a2d(f{smc(*$k|nOonu zS``0D#(cw0=t9~5UwG8A>SxfSu8t*6z>ED+A{x>jO$~k+j)mFIo8eQj+RhLUSLHnS z30rmS|JhmAt$M-f(9P=Mv%>*nV~%w3=51i316Q2>f7{w=&P7ApuhOCK1=Tp10?{kw z{bTzEyLg$$95{5EtN6$-XS%TnH!Vw~ z@D&;6%*?EWkxJ#1gjAB4^G58Da}mm6!yIys*@oH1X563kyC0AHcmLl*A3pD0yWZFJ zdOcsq@`G{EBn2DiJDLgQ`%L~^;#)j-^1U9uLO5J^7TB@T2emd1&;0>_4wp7ohLrAQ zhd!5v)jWL^dKZ!f&!(oh1GbH)F${CPKP^^Rd%N+L>blI`m#|V@nWq^Vm!Hvg#}-mf zj`7_~MQ(hi(X5Sw)dB5r6ziK_^wkmg&sJP=JH3bwYzD(PN)vg&mOLWl>uOB=xu&d( z(Cc+3VAw5@7AqsqCJ9$qx+OTYuleRCxE10Gh|ADst>-aY(kW4|6v_@#>>uODYQuWG5UJn|s>KU}vEwk_!~B0Cn+ zXza+MXr%pwEqi52X<>#Q5HKIwh|Rz^>EL{&4kuV^qmGqW+nKuAP%$Vq;!j&K1Vr2p zQmxncpU|wdehpz4?%zl?@kEHf~@<#zX308hc^S3!LD5b%seT-lmY_ zhVZ^0)~#cP^qR&aW%5^M8-OZOiy)sqp)k)pc3NwPF)E{Qx#-X!Eg`x`eynB)#q4z0 z#D!$$Qt=5Z6p+c^a#HZ-_qy8Hdj+iRr#4$6rq6xydE`2v9_=qm;vGl+pzgvT=IfnOVBU+~0rJLGHM`QmPPk1e(Xu9*F z$!YgHn!AN1xz*X_qQb+}JEN^PX4^_T9I0){@Kg-@&1_Yxbq~tT>VjAuOv37ftT)8G z;_PMA*^`%@7Jtaz0^i7=^fop=mw5$$@Q6x#XG4aht?AU!5LbMOd7$})Otfa!5y++3 z$*!=#A`VF33froJzBxn_dzKct7kM7R(q#G0 z8n2Q#QNoo^QUHuA7CP}tAusZlcRN@nta?qX(h?5`+_GozOfp^moIEzp8|Too(eMp> zoVB$Kwo<$bgcQi$>$jsbO~)7`9~FM_6{wBTp!h!iWHGOGWMVV8sB!qI0jah^h^LCh8L+FKLO~r% z9XW#tHMgOd3|2^5qoEd9gXgWF2EVLuI(fuMeI{4IRq}Rg@7w^`YaWyFp0HhyEGIQ91%RgvOW*VNp=C z)oFd=Y3ukSvgzdw$9`{{?)3Jut{2aLdn+4{1W0;ZF&!#3GyS`PY=nlD$@*>m7g@!* ztD%A&%)_hUijN;VBEIJm3(IeO6N+XMR)eZ-zp5VFa2)c44J>CV|6BbJJMp&W>ZgDV zla=T5ZeN1@`C1YTAUr!DT+4H`m!Cq#eXBhRKv@mdX2E?|A2fED(0>Vswbak+kQ5FM z+u(oHKq96Hxio3mBz`rsVu^YA=Zey$9Bgw#0pC?&(t^QhMM13ZpCOcmJ zD_S*Rt;T<}DFPr6kt=u544ord*VjrcZcbIflRYjRPWuB`ihj3UPf&hmrSJM5a&;H7 z@mcnqlZVmOJW~XLU{heXbZayq0}a3KMHQNw)t2v7+vxAj^0hh*<-d^5GPV$$DXneu z+8gi6Xy8_dI)!a3-G4A~zp92;4mOQm2k{nkhnj(0 z2(8g%)X=LmUjUS2+m^$-GwGk8bV&%3Go1LXy zfD(D``)SYiKFgWOz(sw@FFH*Rq(2LZw`aW?`Ft_1HDbZqqv2>$eY5c69bNrbod8{E zDtN$lJ>@Yj$twEt10Qzhw;9`UxAzANSY^Jf@oJTCERzHFomb)nh6s)liu-%xL>g|{ zMjN&@G<`b;qT~Hp?y0+n_k|MeJ%;WFexQ!B80!`Ag z1MGJhUwNNwWqV%@EU{ks%0bxl-s$eYh0b-_`Lu{q31rlIlwHY|AZ;y26qw$dFmLDX z+lfg7eRf4W*B;5sK8Y&{EbjML?YnF9o6tAMd3kfbmQp2pGN>&KIGD{_DH@^#RwAIG zI@@ufCjdHC5bIsUn<%IVCvuytIJRk1>H{JQ*k|bl;YIK!U0-=>=a!L&+X6kdF_SK{ zT_I;0APWf0l=p769vY9woHE$kE8=;>aQ?=QHsv?<9nIq6n>S;{q_!eO({ZHp_TqKt zmbcUv&?d%!OQ6ZxqG6(xXshHh+I1W% zuNv?5%XzK~EQ&GmEMwxTYR{D7Pcm!Lly21f@j%}@W4HO&S72wZK?7Bl6;@9Iudhfw zA=h!cIdkn(9q85V$TmVzVYw(Qv|<&0opO(R=)P21wCjd!^$S7!bw0PdAUt#JN26$~ zDxQU(DRG<6?rdH-tU<$DF72- zv;T?#o9WYM05ueWH;cI32Swug#`w=_M3pejD1Mq5uRoLSkxu|=Z;t!tc}%?n9))T! zb|2Kh*`khAZ?D500R>CR{~RyH1oxW)J>wAge}sUIX_NcfL>R}Aq(hl>Iat9*%C4U^hF%Z3R=1W)-Q7`;=i06o z`PoUWXByqEfiL>8i5ML`Yt(=bP78UGNwoz+_7QQDvDM5BbWy3IZvsncZeLh zQIJ2_5JjA1beg-r;g_k6Vaf@8V{DMx7@pD4HwMX+b@MIab=hk{wfYJ*2TS1DhR^o~ zMyf5@f(Nbyns>Uz6%ReHh2?x0&74%$!uI8D8`rE3Mh$w=icKr~+&5O$01>mXuxsa* zRq1?Bt&8L8K!fGbVSz5&y{8QjsL?f}zF0@4x{kgx0u(c&2LT0w=ky;}Ruw>d4f=be zwG%G-5W6R~|5FLy3}Q{7%z{-uWoWn75)G%VqAD88!nOF>z*+RD>%*_Z)W-6ljeU8u zKOk+#b{h5yF;D)or>IxlF3m9$-UJ9G)q={YUPZ~V@r`}BG&jISg)yLB#j zioO!MguJqLA(F>^UFgJ1hn9tBH~t_@2sxPAU2)pvp?-8aY@E@(k6h-18dAZ2pz;Wu z(Zkxjr4PElRzgtd@Cw9tb@#=Yqqc8`TYNOX^yP8ysEz4li}efdzAXRTq8%AK^EnWm zg~6#Y9OZNEnn z0GK(%oOvk``Ck!_y9k}wnmR~c-Nf)v5>6__9vH{2;+1Z{N9GadVvAHchp_*#zCXsbrK zDS1HL>u<8vU1mD`e0|I^NAh*b_>@A8;ykp*MPC8YTQ*9>xFmtq5s{eKJl-!ds9~E| zf$-T*W%R`Hnmg{zx_5%*@ZN)Q&`|#S?LFWAx%R)*{8=+=#qJBHTsr2mGwKI|0zEcEeDjFL9+DH_+z$ZW0-Hj|POF3=)wm z_(2@ex2Ul{@LoQ)WxYlg~rwQ!haEoycH{s$3&h`R)ukFQ6nGiEB~?M z&GA2{=leHjt)fuozEi^-B2Wxs&TknqmfF5RH+fc^(PCic>VnC+0>&;)w=hNk`t;W1 zq%Pw>y@;1Yr-h&|{<7Qb_F>cJMSFt?s&bnsi1zg2cVqs_Zj)05k+X@OE=+waptHytu-#dQkfnO+8-`ccL z8#`a#(1%}k)}6G6I}{f^v!-|=1K6`#ZW@h5_l29V#|xKLp(DciP!&x{U}~{_N6+Ht zJ#!D-46FRjxrvE5%)0&?w$PY$?V{{LQb?0mjZ5aw-DI_t1%fzTab zs{F&KQmW5?zoGGrCi0sVXHmMlH5*jFQ=IOj^$IqF?c2pc%se82Ib&Mg#yeamA^S~i zxg9z6PUXr8;FVC?@}8Yp)xW#BU5uxxo_mOrjuV^;;Mdr`H%g5j;scAm)@xmXBcvGS`Vu?ry=#2WJ--vPPG* z*#*kwLPE!^rU?_L=~-)?-{knEc}dwau;1G+4bXO3MQxfn%SHjSv3n|;aS<4Izch4o zl9&~&(v-GRL&ao+Hupq!>aM#(UuOkHT-K%s`mq{Z#MnD$`B11wo%lF^&UqVDtRL|D zf0<*{QxFO%Iha?MmTHY(tLDwNrW%jzx3TCr-7FtH>yDk=!k9Yv_^bN7mO z>UzI?rPo>QGM{JZ3x0=QK5jf*oSnScrEH?Q4QH7$5EQGZ7sA--Ab?h82Tm}SN-dsL zihgK1U!pr%6;XhWf7Lz99^GkR~i=`fSr<=t}6|ZT%AK0EV|9V z8umowL6YBLL+%#aw|d6YSrvHD^OJWeJc+!x)2QP)wo#j~9Y;L&UphcOUAU}1c!W8@wr>D`S@VE~PsqfaH zgow7ez^{xjZ#xLT4^=@{qD1Unt9%c~6Bqvgnxavka2#ZiW{ zgQXnDKttQ}W@jnP_50hv6?yBHZgM-PvBRcTK1K+5cqJ`pzacmuKQc%GQ98^_Vu~6& z@*ErV%dtfx%rPnXc@xeEa1^$!EnPjcMqQ#RvRhVK!yEiXal;b6mljcV4pI zHf+(Efa2Vmn^IMIbV&T}Y>kL-*KYZd2%K>qG?t$ewU(}6L?KKbDUIxu2TqYm69-WW zGuXUPQ(3g+=`q#6HS5*;yYk%s@{BpX%yObmUuC~T<4uJ$b}PZ-M%Z@)7&n_Gs%8^= z-_TNEJvF`TLzo%7h^3v9Jf`nI5>gvC9&;Cw4 zU0?x`f39k_5RjWt>h7bm6t~LPoqY1`O1MP%k!`y&hagLIySs2OyGeQP1WiP@0>n8H zO}K{<@o!3vZf~psJ}JL#Z4D@4uP!>#Pa_xm_kGn2EzG1JR?Q7O9bCxl#n}qIh*Mqs zp3cA9yXi=s#zM;wfv{`Y^_LE3ao}SS*leu|f3JTjlL!~e)0mB~Mj!xa+`(NB`KVhS z&Nw!JuihT^k8`7LTil3W_mnhLUddKc5_!&+5f8kzqdsyt(RT_JWM7mZd3OUUvt;l+MRze!Cs!pcp z*0#Fnv3g6%A{4Zty()W=-n4QeRgF!B=XnuP~@1+TXlGx z#MRhq_fNfZ_K!d-;3yUuZW8gHDu=Pn1KQ&XS9es{AVrh!UxY#=2OpuktJxYE0z;X7wq6e>x=xsKRK_vULY z?jY*VeFI4=62xu#oZ8s;o7Lkaj60k{@tM86{%ngSRCsMmhoL zGLol15j>c*Bu(><(8u+x%l|YVn&>a62bJ-;^PmRnXKNb&b9w#0F3RSYZzk6uZ8yZw zkB6F>zixY7b`lSZJ{d==;WZ0GPOt8wpJ&F&#qCaw$}O74$h7Cy&OWumeOW5<&5u-8 zSJyWq9Uqr62WY<&%d0Hjqj=QJ7~`C^wUF&ACF9NxJEzZzK@yucH|KRd@2f8vSCJ5~ zv=Mfp@&U=w=;f?sV|^Gzq$O~Vg;ukl23K4pB|`Ona=`h;qG&^G>vi_UWwe>8r5o?J z(Y<*7tL%^P)z+CWn@D(h4J|%g!U;7wpMqKX8dtOEeQI|@uL%Q3q7FnJVY{PSx~D<< zd58z4ZnkCk>A;Vv80?Yy>B1VCiq)~_bX&w-Z0`y%ZQe4R4@k7@)7&%y6a5**Z}^%} zz^oHMdD!>y(y95Tn8YsZbmG~#1^-4h^2O0km^v=4a!_y0qg1bz8TAX?74RwgjA$Yc zRzLSk)T8l`g%;`N(1c;Y>HmG`CFCPo5T?RJ?v@p&^91Pt=C?B^v6Go;z{DZ*VH<-X z()+JwDU+?=r$p2dHe&uEA%a35v3PbaAKg*1@0Cd-E$ln+*3$TM&Nn@vITwFa7ta-G z=}3B0mt}oy$}%+-R#sO^NfF1ZR}N^BhaOKUav-QNb(}xs;hW+WSFp)MYUmm4jc8gb zr|l3nm~8X|Yw721Z}2PSo_uy&qrF^}BW1_I7KtGm%ypvp;x*-PQBI-8a=fy7)-vu= z;PqbzC{yiZS4a)9l1N|Vx3eSVE^EsjX#eeeqg75#HFB{CWs-+EJxN6%+Tg_x$y%nY zt>G+rCiubnH!{V8$Tkd2>0oM?MV5{YHP$6KIAU*J==JCwp)9{f|6VUe^z35j0*%(F z-h*(Aqxax3Yvb0N83xLwTX`mW;@b;f`J_Cy#H)0dr8pv>Y^Ws9RfwGy?t& zg&KI29ea#v)#Aai%|Uy|Q%nhW;Hs>H3Rif6yXE_atmE@`~MU_)|rm z0orB`O$^XSUikS-^{=;=la1WSM##Ncyi+^^W3ZF|%ty_UXa$Vg^Z?Sr%Qu1;$8o^h9Hp@F&9b2jKmtJG zbj6UQn*2coV4VE}*fDr$!TS<$4jLQBXh8_~65R;$0qN$Z*7pkhKZF~9O4ppx+LWfh zwp$G-%sLH7{uKEy`GLR!`akYMz`vSLZ&BC^_K*V1s~fK;ql_Z9V_L;6*LH@hjcz^g z{Su1<(0p{wm|v-SEywo15qRKCxxdmP1%AY_zhHx&=yqMb7CD>f`w=a&Ad8T%B^F3o&I{)ihT3P@~WBE9u zrHk!uKA@PDg+DKxP-3o$iR&FXl!`HPUC8HWu;BM3-qAc=nulBZQLmHo3AsL}NS$@c ze;jd3UgAN3sv%NwzB;}j?f-QG9@^cx9R2&#eENcG`MLE6|(dr@X=(32YzsUXad0yuZooZ z2{@&_U&acOmv%7$ms})1>WZ3M_utW4;{-owzJ7~UL7b~2a!ZMeg8gkIahuO*^fjJ7 z3W=laN3K~S2W*$MLQ<^5Md3p&fBntECZ5syc>?QjP)vVrMG<~l zhnExl-Rso*RLa8uH||&<;7z160HZ07cgmU*yX37v8RHN*v~s{C7lq^&BS)TiH%^Xi z0>Qk53blxjbeBnX)WDv`--lYmJ|9s@XjTJk>;Laap@Sky>44uW?(dPhvo*2JLyl<# zg{0B}6*^G4Civw%z=zt0VCGIN0gG&C1dxxC`I>xe>>9IxKU@$YaE<4JYa+I}0=ueF ze@;)W=8vs--!IdEv;Rs|W~p1z(R;{?c_rCZnI;L%K=c+9NaTIvJV=oPNc&#lFy z#81RSjRn9Y5V*Tp^n#q;2uAHjpePNoJ=?QKAy+avgg|x(+S5sa^si>_ z#1kjT=?%Lwe7l2hSCO>=_=Tgve^n0})PV}_EXRJHIjGk!;?XYwDZszZE>S?g-YE{4 z(neH2N~D`@mS@u6_(D(~QeGp|qPes>*VVhi$<&H0YqeNmc*jj~NZ`}umc^>ChsK~PCjxuS*OsQMFZeS*0F&#ALb|bhq*dAZ;1dbs z3pb+{0exOt1Q9rZyz6Z^AYov+2lzvCynfUX`7~2GL8vf!nrj<7B{>H8l)m0bE#BQE zh!q_ZWbBTou3MUMZ*eFSTUcUR3sGPx{_D$F{Od4wtZzQ@dL=-AU#b&aKz?Wh>d3h8 zEP%LJc1fj12;l4GJzPM5A-*+j0DqwGO)ks+^23K) z^^dK0K4$3wnHUM{w9l`GjfrXT@dwFL?VvNj6M*;6E-nGlUYDX?q`G}LdatfVa5!W= zz(~aG%7M#E3$jZ7O&xi$exDLlmpA-3U#V)wu1xfCJRINUnFMG>7P348YnH!=GK5jm z#$m6Y8#i}z zW>5qy@G$QkFdcxj)oLOAzldHu)aK$<5sGd(EIL_mr}<=bQV)E^4gi><5g{F7ByhuQ z%v3EuF=Y_FY>u@q!;{@fk3WI!migI3Gn=$(5olcU#OJ34cz%Q@Q%6IKs2i%#(9v>GC<14~f{+#_ZE7bScTf7tXQ>kFL z%lIil;NpLPw-48~ z*$?bVjnYi{f>cYOl^>lH!K(tX6IujpDD($mT~kvd!>)?puJ!N^$8b%o*fSzCGgW*ih=`8E63S zT2rBLneMKb|S zmocxD;kz}8Q!wCD#Lx42?8Jqh&#(!`hloJz*=$UQcBZ0 z5Ir~>xB7$EPu#m?@#G5JAgI`h+M;Xh0o(X@GuJ@EIF-WyE@d;@0Nvys2Z-GJ3^0ZW zBzTas1Skt6$mhl1wSF^q2wo`c{^@A1vCT37`k+Pt(R_})X!S|Hph2WRcEou_2?7i` z79qCjS0$r-A^_Jg(wsALKt!sFBy~8Tb(NHi@e~(_MCC)=rvK$zpW}14lvEBI!C0jp za#fLL7v)OSPz{hoAzq~;I=f4Rl0MX1cO*!>J*GXD$?Z?ZP*U|uY4l!N)GefJq`oh( zgW`PWR2*@M21)$qI(aFfD#{;msP~Q%M9nxkD2gyv&oi#q`fN-k4UZVreL)f4(G zje`S^4z+)kuK+%r0sXFDKGA;%gSc@7PtBTIiHYY;-mX5HyIQ zR!c%YLS_$19eUC4WoGz51@(;r8m}7I4Ns#smtE&}=}qZ)W;&K3L};9NaQ)8_J~bWKRw%nnl?uu&>-1__v}PqU*y=p z?7qHz^~=~qw=usHktGfRSP7Z*r;kj`&6rKY`2?Vgd^lfPL?e!!h`dgf1)JWGDcE^y z8C&N@buSxdAjtQ@rN}B5D6Zbxy~GwwYT;&xV(+Bp(XVGzv^))=490MW>p1S#ZgD>m z_muqAHlGW7+%4j5TC&}VuB{`vqjxdg2$;v41ah%yPz>S2!o0&5+^j>5NhxYFiEEY<9&Tva3rWk)>Egk|5ONv=Zv#u00Ud2Qr(G`TXv=t{MW9dn zdlDfE$U!$cqEunr^*(Ls6RH%<`pgGEBiuG|0Jxl`5I3GalBqui#3?UqjozsO8SY=l zzW5!YAu`^~o;3mLDp92@?Wg;Cv-CK;ph^_GqOBRS@tbls9)m{}O~l10+h%R#@kZyz z8xe^O>ApT}0Qpch@hsj6nt}C#79GZfC=)#)_~S+W74}Epu*sRbAcIA9l9n zx!(N@f3d$*1wXULKU!R9JSu{6g@8_tGJkt&N|Di&_g`7f8+I1E|b?)%(Kp| z?jsFx?^I=JRI*-*u!4d9!#Bve*BjW{r{UHhjVl#ra|}})>K2C{$KT#tE?u!!b9CiQ zc2(R@{9?>=Bv0`s+H&P{6q!5p-rNY&_B5=oH8P1DSnE;7^kIa~f5@br+O;jht5GbP zkU*uWUnpy}^XMsDnjHqN%yl3%R~^&S!2e$q_Yj0%01eP zh&;3D3gnQ-EwKch{Fs1xgrB9il@OfPvz$sbGjE+v?=u_Z+`rYnugDlku*Occ+-X^P zxSokV8}!snxQ`CI#T6w#_P*7S#QxM1ThS!*1J8&2&<=n{H1-uQx>C1}ES0%AF5(9& zVi#*ZkP&NpA`FXKzIa|3n68A?a#vQ5snMcyw)B>QG{d(t>9PeAs-aG+n}1Nf9(#MO zN%M>IlI&*JX-qB`6o3~rqX{fe6!7V< zh~991ZEY|$if7CntP5RLQ8sb@ z4}wl0m9FJ(14@WL+-caq&GJjon8)e8oxR&#Pz$cLTX+#Kl+|PqX)b2ka^IIo4jB{+ zqcM1cz4S(q37`hl4Z(`QH>WGY)A+nhx_+kgAI^3dd!@v1RK}t^#$yyb5WBh5W}&tz zMoAg~RAy3D?3blJ+lgtk)O4BN)XgV=B1a*VnhPweHx~(1yt*HLpq?T}KCn8m*<6-Z zsmj(45Y`K89<25<=jM+jNrfy7#&zu5*@XwLgdGAcwPMmL7~}pGQyzhiqwczV($+P- z2evcmAxY4ENglZpfM)2ckh1WLAGQJFKK0Y9-y;Gy4Z^MU{-YjHz2e@yy&3FAyiyftPL*53;?%OyYth_(_QlRi7r}5_9e&qVB-#wBW69Dgx``yub z&dK(r|G{-y3GbaW{?oS1>p-0kE#tlclc#NAR$AP{RbO{(0Zfo^4iNDDR+O?I>YUh;sL@g-A=Y}cS>p7P$tKV~U_Gh>sZT=PFI`x_mM7Y|VNehX=TkiyM~hWxrq{-^Ll3pMMCVbRm>9OG(N{sZ>yVoK`q%a&>) zH20jbSgGYe&W){pG7)8gCh`h+U!X`wynY@JSiZLvupQS?0*gDT)<>#<&D<32DL zxKANKu#;f=!V{{-et$&xU|#SY&ijk$N%O+=X^Tq!gn6N3^ixP?)DHi~7kl}wz*UJ& zzo*~mOiX}9BQE8Ac8%B+Okbc!>$eU;qs@s*5AsqSW&h23SU=W+ce7vZ6-hC~e$G*K zIlNzfX#$fkGz-Wcm*Qy|%Ycp-^H1QVX#P=G@2~*NqfS@e@kJ|270>u7OjX7=Pfk>u zT>z|R)HTn{OBy5zIp7SrMgei{`#9}|OKWO^3pH=;AkWI6xGdJm5|I&UzgpH)%Yijd z8GqJQXZRLRB$P1D zC;X|1OVH|_FckTt+SN7cQl`wkUe_?tk$LvlxQVS$K00*JEc)Zdt4|PXSo1`W^V9R} zSSO343VtZ|1R97H1sxS&o!mH6ZLhAtO1yn;a|6haTb$ZoT{VDxs<={R$?|(Lx5W+e z)}H4>j``R)aDejY&#L&uKKnMa zpf3&50e(W_*AqHw#+XlE5p4u*C|`O_O(*F&J9%F>z*Jb@DUO!PO7=^&_!jmiu!y&| zkL|uoK}A;cyJ=6zz5MJ516LKdg;i#o2~Z$=W9BmHfG$MUfaf?;j-<6&w*vE?oNNZ4 zf$76ZD`i>PWWK(%cagpu%%OKLV{QTJSg^xEXUd``eB_CD?c+{`2CW^oMWK?j5Z^ z>L+V6_It0s-{WgTH>%5(|m!&eHTn1_Le0j}DNn)Kc z2{y0F^3~SqJ!vr}dq=V}y7RBOVBe3uD*}5GPsrm-&+Y;I&`-f!bsS#%%yDCASMQFA zw{bsYA<40w>9vh@*B{na+Vf;`cOPjyyP`9h?J-q+cDZrQXkWa8#%E@*?ibJLfIDxd z^u8;}ysAyM9KoOp=S#ltsp>%_3k8h*XYX7|6oBFIPF>W;sQUGc(ipL;U^i%~NOaRr zmzYK6ogtRP*2SAZIy98xDxzYl5~kuV(oFg(`FOs|93XvejrScdGdONKUkl(vBmn<+ z-ZfS67V?|n7uQSSN2*?PR3m2KcwDxV_K_c>@tM~mxHc3SMT z(C*zwX4MWAjSh#M)@Rd!@xaeTSV_2eIOmCrtbu53fp~2t_xEXV*vPP3!6o4mbxsTD5fw77eT%@cxod zt6M_vNwbs3EZV^g2MW32J>>ZmhVmvL8NdHh4~12FQXOg5Tw^@IcM=8%Sx9o!d-J?@ z^*qdOsKhvx1^^AVFN^l{YsHVXD@*msPI1soS%w#)tijWR8*o1}D*mH!={_x#6kx&M#%XEVjZq6(6j$ys#@_hXu$XJ;L7I0?%*vCO@>tjAYihr)jSQK& zPAxc2aaJ0n3CoFa-oK~@HCB-+kP?+kKV$~|g3;M|tBYTY3>>Su^sn_c6~{gIY%0*r zBduCP)M$C?8q#${vGJ#Lmx5xawIqN}=uk4YX8>%2-R)rj@GtD4E5{hPj;L*W=VISq^XYMfdL`) zcQPmC7?38`<#Erad#k2ZV>}??2Pk@Zj#sC@)VP-s)4z!OJwt7gxIs<3Esl)7GoqDV zZ=WE;ndYz0tGe)M1G*tS`^q{ayuCEih$xfbBA)Z?p=*b0Gx$Aq?sDY3`=GsTy3hR< zOnnXbR#Cu0vyTrb6OE4u1y~G{`c{GU;Ce%|4;clS%-@Yudhl?_T0PgXrz^BNy_=18 z0L_}RcQ`oe<0owlP8dr4>L%h8dc(>G4PjqxW>j8E>_)C*GU?d5*FtpVxT(6cXcMhW z3K1CqCuor4b7C$5yaAO4lKLsL$5iS z8woi!YB&1IF^5x>i^*3Tu4vjpzp87rc`w81GE*V;0 zXoFd;h(SVxL}>r-riGpy)AB6?vk|e`B(-Ge=keR+YywYL~4)@Nm*{q8cluM zu7^>O>>+iDq#G^VDo+3*UA?F-EWgmQ3Y{wHq@gAPgi$iJ66mF7%hgWDjpSpD*x=5< zQ>NnnxUaW3OD-lW)mefn8mnD7WCAav%qjt3jz#u@Ei%SKb@A!4M_)x?$pn#4A=0{T zO$0WV+RQYzHeH>0K})tgW`Q>3_x&ihc_#-fCuG2Gaa!-5#)RWFjz?3~9>FMyhZpr? z(sM^Y5DUT=q89beEP>WiJ3l4IPbvP8yf!2%ei>QqC%ZfHvUH1$;P6 zy5nLOl+`t_Qw1_!e0IDPmr=)ksn0pXX9)P^tO(k$^)c4FlGcYxq-uLFnWhw)KWaoG z_h&~VN*i}9d*yYO!gw23JW}z{h+~OkWHICT<^psOExo4*UEA5^^spoBbcNTn_W&({ zZ?&&#XqA7P-icfLb)_s*il0{Xkedk#O#Fieuepy9v#~7xXx9aPzD|naPlfO42F9g= z;Wm3k(vU@Au~P4P^xetVGWN6HE~VpbmUoiZtE20xQLDE}2>ji_y3CMQY_&A7Rn@-x zs$L@aq*Rlb$SP@PmocAqmaWtjeeJ^i zN2^+C%{3>+3C5W)9od%er7?Y`$=H$OlUZ^k*e%<3W1l5Yx6>vg4Qt;-ZJj56*v)sX z)pSg&*pX0x}UnH|PnRhE-&n#Qh+ZsjL#4o`=TSFiNv-vcHKQd($eaHO#SrG7&Q;bp6}vAO~f3H^P|rA(M`-a9$v+uV55pl`figbUfP&UzXSl z3nDO|@|xM4jfxyHusuG<9GEGLnpG0yKUnCK2!oJ_#q z8M=n-oqH4O9L#-t0Hi-!7C0J&F)1NKz=7DjhiQ+*e^YA*V%g?YPI07y`~)jfI#?lM zb7YXc@(0x&Ug{%|vSC@xm4L}cONEUN3g3m(0Xo?y?cZmQo&>!z zAb}P4Udg&An~>3c%`ejvh7Vns6BCT<|}Y=ry9*vzlp-B4Vm&2QBGvpeQ)`7AT+k|Oe6&&WK_ z4SxOMRRU?}zcyCUv#(Byl~9$Li79nygTEsT@n)~e&78tSf(eznVXr()hYa4;uNP9`O!G}u`$XeJ-`PNe|f#M z?L)k@%9M_?YCsmVv!MRn=4QLv7foh-iYVi{=P~nNO^mGc&#vjpl2r{sH^y77pFHn{ zY59J%Yjs(eyzm}BAgLbNB$bgI*-QQQp?=de zz!6Q88d86lcE})UN;3Dj@t>sOKAw z=e_xKpeSNaIdz8UYLVJ4@Rxy%DVwPu-gJ*in_zq=tWJy`N6xY1_YmCIS(B-34K zx6GC{G~`-+CKo5DtN*fhb=h&ZVeDHY#K?D|oU`R|DotTkI2-29hB$22)};vxZ8R1A zR^vNret%luB*XiYcl@cQBj5wIi{=#1>^7AQRq;%@<(qP*oa1RYOV*)PO+@pc=VOya zg|&s2d4+$*g+pMDJz#%PhOUpBYra|Er1DN)(C|~8EVxWRrd_}N_MB+DJ=ls&T-Y{1 zd@>Joz#pPIkC-%~w0fjiKs5^Ugp))P-bfomuu=c1#WqR6c*R+eWVhWCU>JH?Y@S(A z#Rg|#JlatvG|AO(TbNYN#3j@#HN^{nFL4txp*ECPP>D9cG!#d(To zR5W^9QW!}CmvUDAIRQ>Y!v zUL4Ilm8lOu9xP$;LG+k}#dG*Yp>vwfh&P^&_URC8QLUv(}h%6x|GG+B8IWZGl04y9>$I4E=mm4TCgt3aQ*+__8XytUbCosI(? z>nX98zxr%)=Ht*lQLoECoQ}2nH5VBkyLmjj(1w)qBLEv=_U_~LhDVNWD_8Ar;s|cr zK+M8tX`Ljpl#L5!7hhVdsQT&kvGVAb!;&Fo?t=?5kA;_Vj~^G2w@1f5(680LvICNM zUq^*RV)caimyLT)l|o6D%x9moA7vxn-DqWXdxwZs&yE5A!XE|xKkU7CR8!m5|9z~$ z0V$%OR0Ty45CNqopdg|YQ9)YhNDW0mN(e=zOB3l`sS1P;dI_LZsXHUjb~@t(pBti19=XKjfxS7-h^C5Y-WkUw0s?*9tThghb7nE-odMJ<^*WU z8g6NKnhUq!RkoTMLZ%8my6UMZ7%rMd*`>^5TY@HJl{>QV~aqh;w*_&1OwqaCJ`SqqC z@C9|)#V4cpP^z$*Fo-FnL@pn+f^g-CLgYn2yEat0nS&roBD0ngJ3$1+7{hejjE`OE z)_d1Y*SDm|tr^k3LN=k%a6y6kwL+<#(?pzN_7n(P9)u}}&#lgEL@|k((g-YuPu$nQtMGcZD#&a;rq;pW0XtHSZ(^8@ zqw@Nt>h*U{VH2V`&1AZ0u)~&tOQ{EXXJ%;AbnZ{dDYTJ#b^SO3KNT{39f^IA3yGD@A&y51ze@LRyWDhfGKcAce`dU`AEtc? z8Z+dP6S#Cw@B}=zeQI5StJ?z6@g~ebxci*O@lKjXuaJt)s^G-q$Dlujo`N33lzSj- z&{7N8OiP)XtJ5r2BF|6)Tz=Ayk?((R%gU^-3bTeBSYN`NC}EU875g~A zhunN{ggzD%;TJgzrHc{p@83PbYG+&AxtmeWl9f1DIOl^3;7bvCn}6r|O_tjogC3#= zqNY>DDyR60bHnhNbm0%}xwRreYp*;Ru1U8l(E3+;*bWJMHGI;GZNW=<8B{d|ylX2f z4OB@kDr^5}V}AGI>N77gE7;3hZ2jg;OlztpWcC!vXuNoM0wg{`{|8@~rQi)_ z7TQZKXQcW%6=`Lc0uZb$i#r4$^3GljJt|eFLrciPh^5(&{QDf`=aSMR={fufMs<05f$S%rh9^(ZtIh*6J-*$)LNcF{@j(6%S zmm+P8Z*T!zZ(lMyJV8pyRhVJZ(q#DGfoZiE)U)=btUQA~yAqQ5w zIryM+bUg3t!LDPfP%hA~!PCL!yNCJhKzB=4F_uVT#6aB2i!|p`(V~hV`3TF!AxZ5E z&_wgYSKA}unewcOZ->_w*^y+)e5VkxVgJg5ckCbMV22>nqB$X;oDla`=H4Yfh%m?{ z@m(jRjO}|H^L(AD3DLObK~a$Csb9r)1^m{0+Vwh(K@?gW_#dO^R$U!Mdlbhr9C_9J z!X9*bFRB)r`RnqI6o3`Nh?OsFgC-f)zcdeKRFey@zQ=BJ^O=IgMf11CMY~AtAXEBZ z<<>JqGRS=Fqzr_9XL#)MqwhCnQv9G%YGaCvsb#)#5c3c&=iB4Wj^}2f z-BKZ|36<`5Ixv{Vdbw-$?P`a|DuD&#Kowq6?7Zutn6JIS@v0^pw2^bu;g7mKemx~2ynyR$cV=U zy@vgQNR4yV^a@U7(`TtT8Y7qKhCcHyK!Gp*LeiRtWQn8sWRn$0+^i#}YC(Vot*UXK zfdQ)V2KGaSmanDe3zQOI(l;E@=7wCWg^o{A0#g$`WN7-8}nR^HUj{pKyCggZV3`QZ?FBb>c!7 zHt9$vXv z+~9+3avQOqN?V7{7crH7EoPCO&31nHA}S{IkxuapmxLDA<&-k<^2PEm#a!$5$90RV zL_CKH=K|+0>NRC)VCft;1ojaf7mkhI8xbGl`KU6^yZXY)M&Ny=N)o0QjsN6A3QQz2`s&n!o8H0rBG+|_ zGgvymba9#3gT&~p$)ZIwDQ_E^zn^8TECBAR=Q^h?8c4SRE%bP^YwRrJQP)o zA~8cNt~#p0G+Fl48uLj7U9Ldn(hgXPAs-iK>J^I)gWv|6`yrJ&y11XbZ>z2^uH%JJ z*u`)ZdyPB1U_hBxn(>8(Z(a`^CNX=YJmd@k=|L>h*c|j5fBraw{&^}EU1Ax65L$Fh zdJ8_A(r$7KOM;eJAkG_XRa=LOH~KpCund>(e};^5+InWtWxVc$j3*L1cdQoO9KPw- zF;JL*(ix3Et~}ktQ;eM(hxy+TNFft7x-acWXXIyrN^sGj8@L)m1#*IszNrV;I3}my zM|P+?u09%;QGB&!{oZW4FjdS4y9oL*FUd0GaqMb0ABv4UHY?;+n2Nw}PDyG(-~((i z`f_wyXd}AVs=ZMIudWry+DEVV`0e-$2SL#ovaj(KIqY!36bsWBVdPU9uwyBH*}=vs zWb)6k4I~EDKfb*zJkDt<&e|2dhb>|lGfiF&#Z1B7HGUhr zO9%CvyM;4Y$;xIX*#*Z?InoZDi zyZcm@0fi9_qYm=4qv_{3UB$Mq9QZvS3eMQgF=)J`VCT>(9W8k{5NYT2|ESD#w;KfcGJE=8w$+W+?HSgUAli@>Ws&3C7qfW7=DQ_YgU z(Mj&t2dfIPWK|UTGqCcp@`vQEo$S=DRuSaQ=vqedK+NscU@0Fi+)UiK=egA!Zf;_@ z*r5fkL0~w?x>CcM_=00CFASHlDrE#D=0y*kY1aJO(tqW({&T)7k6_X@B_TgQhVbCi zeu|xi`bI=n<>YQ0X-i?L3OO3~8fqd$VM4e*(PYEq&jpC8N4W zvj0i=pVI^||73IhkKk@r)y^DM9t~HByMeMsrJhsGgh_j|LjDhaePvAb=PYHYgl^@` z3;k)0qKhVa+S@c%vY+7VYW>BF;8*Q}3m5XLW&_#_vA8Br1*IH2T}oqLUw;BK>5ZJb9Da|34COpqncZU>u%|1=9KVaMZcn;)+UZ%N zKB24fnl^Z{UH=NtAl0^zi{8C9m}Cq-s{l%cNJ+J*XQ4=EgT1C^TA7ezDjEJ8i#A2| zgWbgFJZyOVoqGKZi!OS3GJ*-Kx5!SnXQiS<1qnyQZvZpup(?w>44^ohKDV)?>Fb4@ zG}BGcbq$hqNSrU6ZbZ9{?`mT+iEVK`#ilVl>3y_?DZ*Pgq9VV4A`({QVOJ4;*~Aui zd=KPP-R*`SZVjn7G!GVRU(ZrG2q^@Xc}dMX2sY35B~)I6$;C76U%NspF8KW{X)C*z zd;#etut14SjE}DT$bEKvs0Mu${H?d!0^QTJlb$jHOKo-J_w1>n+?7lC_p2&z_?&gR z+F(CeRzp7pKID0eSPg1a0E+y-Z+ExoG({rLg(@LRP95*f%(Ts|Yf-W0-CO@I2Dvgu z)|Fh_P)md?^t$&Zf6)Nd??zgi#026^i}F@@d1dPw&Tqf7ESd->5O*J^?(YVV{N19>SGh?^)i`wNiF?pB$K{S zoz*nkwW%o0v3&u_6-%C6M2YBZkf>S!`$=GXr9GH8o7;o-30AeUxXw^|-GX->rsPMz zZQ;4>A4|lnmvXD`j;q#p3dfYk)$diPN+4kt&~cg_3Px|V^_i}J~IP`tV=(D)PRB0)@usXxi^`#A#q_1Q@N(T?Pej=7(dSNwNEL@=d z%%=FI|W3dQ}`3puP%8j3(Q+xi2(z_VD|`@rDd z+3pd#_eUb^_Lt&P0Q8$x>**&?>#H0b9u!g|KQakrWNl=)PnYHAWj|A#gGnNNaTfh0Xdt{r zWkBkKg^BDLLzTfl!i1=Mr+F1z9YTOjYa>-~s3p)EP-?{IVBtS2Aw{AY6&ndX?B%)r z*s!Dug0->9#*!zC_B!e!s&bQQ&&S;cG-}UzyQ`cP(&2TNkn9*9q?>fP-w*`NwVO~o zZA-Ko-kW_0gB2JCd`E(=8* zeB5eC%Cx#4OpXyAm+h?;kFFnY5|0)ahD^@@d&4O~08dH(Jy3sf{6yyZ>#MA^793)g z;!E5QZTS{cZVtH&O)ogHKMk)nF3A-T?= zb}oU1t@kZ#Xpjb1m_$YXo7W}B#8FNY66)JCw--KLdnh2@@I|Y->WY7I^pzp~(q4s} z=n~98qQ3eqzJiKkgVS@pmbpu71xv6(#>R7vwysNQ>jF~AW@2ZDc>pOQau&$B3P1Dm ztvTzN5_x4rb@9mk;F)&_l@gH0eAz!qk>-?6eA8VVqN)F2TlP`DpySABv zi7wcm8!dRmBKycAmdr(uiYIJgG=Mlj{qsIm+m&AD=fotK7wQ>En=r%<7ZwwTY-M9~ zRyHbSg?zIu^-Z*it#2>$#fUN$1z$)A@f&TR9L+0zb^4N2#~Qp$O(pN)jgUQzdesh9 z`Vf=rVgBEr`Y){WeKQ~XAPQhj*YQ0Th=Z}DL36GW+pgDE;%xl{c=4(rKvAEzNyUOt>0dg05JMTD*fAjTu zbk%3NP`Y9!tC9_KnFo^2`p5sgJL&gnt_o1+k$RkzvI8vyX4h5FXkDfe$Dyvz8$1GSWPf;_L5j4~y#dBcJbtFpz~ zP_jS}`G;GE^t;M_d)D4v%?s@}>+#_SuhT~!nVZzuvf5Im+J>Kx=rSre@Ke#jF!9I= zJ7AZHFD*-+pMhl<+I0}O!3>Shc01}~&vIPSsSU>YXCAFRMkZgw9?fCGwcP9Qf~_WW zOo&7`poHQ2!U@b}lOpwLYcxv_OQse5=wzit7 z1($miR&yS4H6Pg8*T*^1(z+rXX*Z)mpSeZ9BO9Xq+dE5#n7&HJu$SIi71Wv7azC|J zAnZOb>jLPJ+VrTsC$T{*Ot-PR!;>#^AKvAi&W#mk;arWWsWtuF1+`b~%bINtFGa~n z0c8l8=Wn2s=2SA;_EbeiAC0xsgQ!T7GkTvBetPHv99Cm?ijgK1bqn^$hxR+{k3SAK zAKtsGbAeZg-~32J4!P^w%>&IrMa%&=mK~Jpkp9AeCK%B%EYNN)Y(~j|q!YcN?w`js zRJKM&dvlZmB{D}e!(*Bov&W*tg2GZd5~4*k*FvXzY?02eu0YS%oXSGW%eg zb$blieO8SxGj8u(4!r3#SS1n_eVlCf1t#5o`&}1ZVy7{#{!;9Q# zo1BghR-d*mO@K-1L}?fbPXQxN$0z{!&5b%OV`5!?i4!(^HoUYHJTT8hLmVK zbhY3Q1_YYcmlQ<5XFTYo=p1!afrY&&z6{vH0B~>PFoehZ=ck`Hf!=ChGE%RMdo@S$jg3E6Qx)un1!hOf^2!U%DH(=hZ9;os-=&g+_;|l2 zvPx9K&wOeE)rbHaj^0wv7-%uu?PGKHC`-Lve*&C;q>o89iQf|y6N0)p*R>j&S2%B} z-KlDQM3g0Fr&THl&IEpR9NGe8gDVF|(eVnv&@pwzv?4~ z{Iyu1;WQ=94f~yW2gZ2Bp16WV-ImNGxW1dEQDTj2zt)xR*Kolp`UuxjRNb_ZF^kYKF7 zsvWi1`@Dq5N4;(ilk2B((hJ7yNaucfS^&`*Su$g*`JOhlUyPA2`e9vI=F8BooRI1{ zyf^I9>u-l=hFhI2jQdrWP%WnT+^)bpqbge7q3I5U2n?e}!xHk~>uBfmkUOBUXzg^s z-_esWu#oDUiNHyLi!I(#AI8R1Wm&f3E;50opjk>k1nw{?a!9BY-L|`t5uNav)hwDQ>Du>buJTu-YU(SqVeK8W{+CNFrPm?++#+#E)i1YSjP+_4q)Zf$` zjIXQwO5V&pdDBI>3VX$eCmfhL{A7o&*<}dI#7}=NdNddbf+q;FHahDT1vL!1TKX~$ z7P6Z-RBoy}JXR!~>XFC-)GH!OrTIc-1`A6?s4=8#n^zsizZYaq!-N@K7V_z~my{;& z$!v1HKv)hpcgGIxN=elm5xCrkK0Z7gAyh z2`&ebh~6WB095)S6BshWN_jf5h@hna!xLCi-r>7;q)^W zd?oC!GkV&f`rBMU@4V%WD_e}+Q_Apc7))6C?J$wKx z{7I$C`o7L&i#MsUC?#ckU&zbcT%E@w7a_FaZDi-3?D5eTH9jTN=oc05EfJ-|mYV^} z-n_WCvWHipIs#BrW=7j1b@EUqf#(9RPn!y`X{)F?SHjLDeK5eApd}c}&phI;dsuX8 zttqGVUPoZ2pm~;(k2viumRpz7*xUIu6d1-}`4@+Uc>|t8gsXWO$niA~vtO=$L(JT~ zB=GYrX)qjmrksvhz(nAU)mhb-oZiZ>*<-|bQm=$HUkMI%P`Z)X%lZi>EMpj(!wNtw z>XR%V-(a*8z^jE+zxBGmWT`qQ$M-4h)BeUyjvF*p;5QQNnPTs?Dr7BEy`S=czUDOh zhDLP`KYuW$j8xycktv`NDAE+*TArWgdVPvvFP@GGVyfC^R@1B=h0#?$=C%sWO2}y0X6eHPJ?WFe8BGYQVu5_L-|$-*HR_ue z#ZkPQA8585ao^R%Amr(QGx2*f;P(5&Iw%#T4hb>765G=5sIbNL8`M1(fXVFM<}@OW zN{M6`J%$PsRM&W_yT`Lz+;vCs1Mjma#!;6+6AgjwWdu6D4qhhdab>c1AvS^f>Ibj@ z2~-#4zkQ=UWgxn;=QSP3?hNp2RxqrI_H4P~swR$AEq`u3+q)|DNdZA?3)}Be=GA&3 zM13WplhCOn%Q}oUDwX^Cy?_@361zO&xS(E6XT{Ix0D@@`t8)C-a2<9-1 zg^TVy))iX8ZfL+u{`^*^ELXvA*8~)u{HUL5+q?Wz4}`2>vg?}|eEsqhtJM#*�Q+ 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 Date: Thu, 22 May 2025 12:43:58 +0200 Subject: [PATCH 53/69] Simpler trace output files --- Tools/TraceContract/Output.cs | 28 ++++++++++------------------ Tools/TraceContract/Program.cs | 4 +--- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/Tools/TraceContract/Output.cs b/Tools/TraceContract/Output.cs index a5d09bb1..4c0fda3d 100644 --- a/Tools/TraceContract/Output.cs +++ b/Tools/TraceContract/Output.cs @@ -1,5 +1,4 @@ -using System.IO.Compression; -using System.Numerics; +using System.Numerics; using CodexContractsPlugin.ChainMonitor; using CodexContractsPlugin.Marketplace; using Logging; @@ -24,18 +23,16 @@ namespace TraceContract private readonly ILog log; private readonly List entries = new(); private readonly string folder; - private readonly List files = new(); private readonly Input input; private readonly Config config; public Output(ILog log, Input input, Config config) { - folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + folder = config.GetOuputFolder(); Directory.CreateDirectory(folder); var filename = Path.Combine(folder, $"contract_{input.PurchaseId}"); var fileLog = new FileLog(filename); - files.Add(fileLog.FullFilename + ".log"); foreach (var pair in config.LogReplacements) { fileLog.AddStringReplace(pair.Key, pair.Value); @@ -101,9 +98,14 @@ namespace TraceContract public LogFile CreateNodeLogTargetFile(string node) { - var file = log.CreateSubfile(node); - files.Add(file.Filename); - return file; + return log.CreateSubfile(node); + } + + public void ShowOutputFiles(ILog console) + { + console.Log("Files in output folder:"); + var files = Directory.GetFiles(folder); + foreach (var file in files) console.Log(file); } private void Write(Entry e) @@ -116,16 +118,6 @@ namespace TraceContract Add(call.Block.Utc, $"Reserve-slot called. Index: {call.SlotIndex} Host: '{call.FromAddress}'"); } - public string Package() - { - var outputFolder = config.GetOuputFolder(); - Directory.CreateDirectory(outputFolder); - var filename = Path.Combine(outputFolder, $"contract_{input.PurchaseId}.zip"); - - ZipFile.CreateFromDirectory(folder, filename); - return filename; - } - private void Add(DateTime utc, string msg) { entries.Add(new Entry(utc, msg)); diff --git a/Tools/TraceContract/Program.cs b/Tools/TraceContract/Program.cs index d239373c..50ade56d 100644 --- a/Tools/TraceContract/Program.cs +++ b/Tools/TraceContract/Program.cs @@ -55,9 +55,7 @@ namespace TraceContract Log("Downloading storage nodes logs for the request timerange..."); DownloadStorageNodeLogs(requestTimeRange, entryPoint.Tools); - Log("Packaging..."); - var zipFilename = output.Package(); - Log($"Saved to '{zipFilename}'"); + output.ShowOutputFiles(log); entryPoint.Decommission(false, false, false); Log("Done"); From a4342273f23700a4f3d132ad2a169d120cbabd1a Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 22 May 2025 13:55:03 +0200 Subject: [PATCH 54/69] Adds retry for block transaction fetch --- .../NethereumWorkflow/NethereumInteraction.cs | 8 +++++++- .../CodexContractsPlugin/CodexContractsEvents.cs | 8 +++----- .../MarketplaceAutoBootstrapDistTest.cs | 5 +++-- Tools/TraceContract/ChainTracer.cs | 15 +++++++++------ Tools/TraceContract/Output.cs | 2 +- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 6a34e848..47b7ee20 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -146,7 +146,13 @@ namespace NethereumWorkflow public BlockWithTransactions GetBlockWithTransactions(ulong number) { - return Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(number))); + var retry = new Retry(nameof(GetBlockWithTransactions), + maxTimeout: TimeSpan.FromMinutes(1.0), + sleepAfterFail: TimeSpan.FromSeconds(1.0), + onFail: f => { }, + failFast: false); + + return retry.Run(() => Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(number)))); } } } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs index e2d8a11a..6adf4ae7 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -19,7 +19,7 @@ namespace CodexContractsPlugin SlotFreedEventDTO[] GetSlotFreedEvents(); SlotReservationsFullEventDTO[] GetSlotReservationsFullEvents(); ProofSubmittedEventDTO[] GetProofSubmittedEvents(); - ReserveSlotFunction[] GetReserveSlotCalls(); + void GetReserveSlotCalls(Action onFunction); } public class CodexContractsEvents : ICodexContractsEvents @@ -100,15 +100,13 @@ namespace CodexContractsPlugin return events.Select(SetBlockOnEvent).ToArray(); } - public ReserveSlotFunction[] GetReserveSlotCalls() + public void GetReserveSlotCalls(Action onFunction) { - var result = new List(); gethNode.IterateFunctionCalls(BlockInterval, (b, fn) => { fn.Block = b; - result.Add(fn); + onFunction(fn); }); - return result.ToArray(); } private T SetBlockOnEvent(EventLog e) where T : IHasBlock diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index 39d2e1b3..390e138b 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -266,9 +266,10 @@ namespace CodexReleaseTests.MarketTests // should have filled the slot. var requestId = r.PurchaseId.ToLowerInvariant(); - var calls = GetContracts().GetEvents(GetTestRunTimeRange()).GetReserveSlotCalls(); + var calls = new List(); + GetContracts().GetEvents(GetTestRunTimeRange()).GetReserveSlotCalls(calls.Add); - Log($"Request '{requestId}' failed to start. There were {calls.Length} hosts who called reserve-slot for it:"); + Log($"Request '{requestId}' failed to start. There were {calls.Count} hosts who called reserve-slot for it:"); foreach (var c in calls) { Log($" - {c.Block.Utc} Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); diff --git a/Tools/TraceContract/ChainTracer.cs b/Tools/TraceContract/ChainTracer.cs index c678667a..ec723084 100644 --- a/Tools/TraceContract/ChainTracer.cs +++ b/Tools/TraceContract/ChainTracer.cs @@ -36,7 +36,15 @@ namespace TraceContract // For this timeline, we log all the calls to reserve-slot. var events = contracts.GetEvents(requestTimeline); - output.LogReserveSlotCalls(Filter(events.GetReserveSlotCalls())); + + events.GetReserveSlotCalls(call => + { + if (IsThisRequest(call.RequestId)) + { + output.LogReserveSlotCall(call); + log.Log("Found reserve-slot call for slotIndex " + call.SlotIndex); + } + }); log.Log("Writing blockchain output..."); output.WriteContractEvents(); @@ -67,11 +75,6 @@ namespace TraceContract return tracker.FinishUtc; } - private ReserveSlotFunction[] Filter(ReserveSlotFunction[] calls) - { - return calls.Where(c => IsThisRequest(c.RequestId)).ToArray(); - } - private Request? GetRequest() { var request = FindRequest(LastHour()); diff --git a/Tools/TraceContract/Output.cs b/Tools/TraceContract/Output.cs index 4c0fda3d..255bbf3e 100644 --- a/Tools/TraceContract/Output.cs +++ b/Tools/TraceContract/Output.cs @@ -113,7 +113,7 @@ namespace TraceContract log.Log($"[{Time.FormatTimestamp(e.Utc)}] {e.Msg}"); } - private void LogReserveSlotCall(ReserveSlotFunction call) + public void LogReserveSlotCall(ReserveSlotFunction call) { Add(call.Block.Utc, $"Reserve-slot called. Index: {call.SlotIndex} Host: '{call.FromAddress}'"); } From 9af735e9df03a590133d2b31c750656a856be54e Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 23 May 2025 08:35:52 +0200 Subject: [PATCH 55/69] Adds non-interactive mode flag for hardhat ignition --- .../CodexContractsPlugin/CodexContractsContainerRecipe.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs index b46f3c96..eadbf4e9 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs @@ -29,6 +29,7 @@ namespace CodexContractsPlugin AddEnvVar("DISTTEST_NETWORK_URL", address.ToString()); AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork"); + AddEnvVar("HARDHAT_IGNITION_CONFIRM_DEPLOYMENT", "false"); AddEnvVar("KEEP_ALIVE", "1"); } From 5944a0adec82329c19b130fc16e8cb81cbcc4df8 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 28 May 2025 15:18:30 +0200 Subject: [PATCH 56/69] Fixes case where contract chain events are never fetched --- Tools/TraceContract/ChainTracer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tools/TraceContract/ChainTracer.cs b/Tools/TraceContract/ChainTracer.cs index ec723084..fc69dd14 100644 --- a/Tools/TraceContract/ChainTracer.cs +++ b/Tools/TraceContract/ChainTracer.cs @@ -59,19 +59,22 @@ namespace TraceContract var ignoreLog = new NullLog(); var chainState = new ChainState(ignoreLog, contracts, tracker, utc, false); - while (!tracker.IsFinished) + var atNow = false; + while (!tracker.IsFinished && !atNow) { utc += TimeSpan.FromHours(1.0); if (utc > DateTime.UtcNow) { log.Log("Caught up to present moment without finding contract end."); - return DateTime.UtcNow; + utc = DateTime.UtcNow; + atNow = true; } log.Log($"Querying up to {utc}"); chainState.Update(utc); } + if (atNow) return utc; return tracker.FinishUtc; } From 830c392995eaf0cf1c43e49c22ff7c278ee83f7c Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 10:59:10 +0200 Subject: [PATCH 57/69] ContractSuccessfulTest - request failed to start while some slots were filled --- ProjectPlugins/CodexClient/CodexNode.cs | 8 ++++++++ .../MarketTests/ContractSuccessfulTest.cs | 13 ++++--------- .../MarketTests/MarketplaceAutoBootstrapDistTest.cs | 1 + .../MarketTests/MultipleContractsTest.cs | 2 +- .../BasicTests/MarketplaceTests.cs | 4 ++-- Tests/ExperimentalTests/CodexDistTest.cs | 2 +- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/ProjectPlugins/CodexClient/CodexNode.cs b/ProjectPlugins/CodexClient/CodexNode.cs index 14c33fbb..4a444117 100644 --- a/ProjectPlugins/CodexClient/CodexNode.cs +++ b/ProjectPlugins/CodexClient/CodexNode.cs @@ -318,6 +318,14 @@ namespace CodexClient log.AddStringReplace(CodexUtils.ToShortId(peerId), nodeName); log.AddStringReplace(nodeId, nodeName); log.AddStringReplace(CodexUtils.ToShortId(nodeId), nodeName); + + var ethAccount = codexAccess.GetEthAccount(); + if (ethAccount != null) + { + var addr = ethAccount.EthAddress.ToString(); + log.AddStringReplace(addr, nodeName); + log.AddStringReplace(addr.ToLowerInvariant(), nodeName); + } } private string[] GetPeerMultiAddresses(CodexNode peer, DebugInfo peerInfo) diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 51f9b79b..661ff7f0 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -4,11 +4,9 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture(6, 3, 1)] - [TestFixture(6, 4, 2)] - [TestFixture(8, 5, 1)] - [TestFixture(8, 6, 1)] - [TestFixture(8, 6, 3)] + [TestFixture(5, 3, 1)] + //[TestFixture(10, 6, 3)] + //[TestFixture(10, 20, 10)] public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest { public ContractSuccessfulTest(int hosts, int slots, int tolerance) @@ -43,9 +41,6 @@ namespace CodexReleaseTests.MarketTests WaitForContractStarted(request); AssertContractSlotsAreFilledByHosts(request, hosts); - Thread.Sleep(TimeSpan.FromSeconds(12.0)); - return; - request.WaitForStorageContractFinished(); AssertClientHasPaidForContract(pricePerBytePerSecond, client, request, hosts); @@ -76,7 +71,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() * 4; + return Get8TimesConfiguredPeriodDuration() / 2; } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index 390e138b..c14222df 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -274,6 +274,7 @@ namespace CodexReleaseTests.MarketTests { Log($" - {c.Block.Utc} Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); } + throw; } } diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index df71eb3b..99d23554 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -6,7 +6,7 @@ using Utils; namespace CodexReleaseTests.MarketTests { //[TestFixture(8, 3, 1)] - [TestFixture(8, 4, 1)] + [TestFixture(12, 48, 12)] //[TestFixture(10, 5, 1)] //[TestFixture(10, 6, 1)] //[TestFixture(10, 6, 3)] diff --git a/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs b/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs index af213342..f56ac0f5 100644 --- a/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs +++ b/Tests/ExperimentalTests/BasicTests/MarketplaceTests.cs @@ -48,7 +48,7 @@ namespace ExperimentalTests.BasicTests foreach (var host in hosts) { - AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance)); + AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance), "Host initial balance"); var availability = new StorageAvailability( totalSpace: 10.GB(), @@ -66,7 +66,7 @@ namespace ExperimentalTests.BasicTests .EnableMarketplace(geth, contracts, m => m .WithInitial(10.Eth(), clientInitialBalance))); - AssertBalance(contracts, client, Is.EqualTo(clientInitialBalance)); + AssertBalance(contracts, client, Is.EqualTo(clientInitialBalance), "Client initial balance"); var uploadCid = client.UploadFile(testFile); diff --git a/Tests/ExperimentalTests/CodexDistTest.cs b/Tests/ExperimentalTests/CodexDistTest.cs index 422b5bd7..77345297 100644 --- a/Tests/ExperimentalTests/CodexDistTest.cs +++ b/Tests/ExperimentalTests/CodexDistTest.cs @@ -95,7 +95,7 @@ namespace CodexTests return new PeerDownloadTestHelpers(GetTestLog(), GetFileManager()); } - public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "") + public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg) { AssertHelpers.RetryAssert(constraint, () => contracts.GetTestTokenBalance(codexNode), nameof(AssertBalance) + msg); } From de49328573d95fbcda1a0786bd81454a7c825dba Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 14:38:02 +0200 Subject: [PATCH 58/69] Cleanup marketplace tests --- .../MarketTests/ContractSuccessfulTest.cs | 3 +- .../MarketTests/ContractsStartTest.cs | 68 +++++++++++++++++++ .../MarketTests/MultipleContractsTest.cs | 10 +-- 3 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs index 661ff7f0..9a816a9f 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -5,8 +5,7 @@ using Utils; namespace CodexReleaseTests.MarketTests { [TestFixture(5, 3, 1)] - //[TestFixture(10, 6, 3)] - //[TestFixture(10, 20, 10)] + [TestFixture(10, 20, 10)] public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest { public ContractSuccessfulTest(int hosts, int slots, int tolerance) diff --git a/Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs new file mode 100644 index 00000000..9099e59d --- /dev/null +++ b/Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs @@ -0,0 +1,68 @@ +using CodexClient; +using NUnit.Framework; +using Utils; + +namespace CodexReleaseTests.MarketTests +{ + [TestFixture] + public class ContractsStartTest : MarketplaceAutoBootstrapDistTest + { + private const int FilesizeMb = 10; + private readonly TestToken pricePerBytePerSecond = 10.TstWei(); + + protected override int NumberOfHosts => 5; + protected override int NumberOfClients => 1; + protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; + + [Test] + [Combinatorial] + public void ContractStarts( + [Values(1, 2, 3)] int rerunA, + [Values(1, 2, 3)] int rerunB, + [Values(1, 2, 3)] int rerunC) + { + var hosts = StartHosts(); + var client = StartClients().Single(); + + var request = CreateStorageRequest(client); + + request.WaitForStorageContractSubmitted(); + AssertContractIsOnChain(request); + + WaitForContractStarted(request); + AssertContractSlotsAreFilledByHosts(request, hosts); + } + + private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) + { + var cid = client.UploadFile(GenerateTestFile(FilesizeMb.MB())); + var config = GetContracts().Deployment.Config; + return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) + { + Duration = GetContractDuration(), + Expiry = GetContractExpiry(), + MinRequiredNumberOfNodes = 3, + NodeFailureTolerance = 1, + PricePerBytePerSecond = pricePerBytePerSecond, + ProofProbability = 20, + CollateralPerByte = 100.TstWei() + }); + } + + private TimeSpan GetContractExpiry() + { + return GetContractDuration() / 2; + } + + private TimeSpan GetContractDuration() + { + return Get8TimesConfiguredPeriodDuration(); + } + + private TimeSpan Get8TimesConfiguredPeriodDuration() + { + return GetPeriodDuration() * 8.0; + } + } +} diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index 99d23554..aae63ecb 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -5,11 +5,7 @@ using Utils; namespace CodexReleaseTests.MarketTests { - //[TestFixture(8, 3, 1)] [TestFixture(12, 48, 12)] - //[TestFixture(10, 5, 1)] - //[TestFixture(10, 6, 1)] - //[TestFixture(10, 6, 3)] public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest { public MultipleContractsTest(int hosts, int slots, int tolerance) @@ -33,7 +29,7 @@ namespace CodexReleaseTests.MarketTests [Test] [Combinatorial] public void MultipleContractGenerations( - [Values(50)] int numGenerations) + [Values(10)] int numGenerations) { var hosts = StartHosts(); var clients = StartClients(); @@ -58,10 +54,6 @@ namespace CodexReleaseTests.MarketTests }); All(requests, WaitForContractStarted); - - // for the time being, we're only interested in whether these contracts start. - //All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); - //All(requests, r => r.WaitForStorageContractFinished()); } private void All(T[] items, Action action) From 9c1a0b6942ef8f292ecbc94c9037fe48ce3c6f93 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 16:37:20 +0200 Subject: [PATCH 59/69] Sets up repair test --- Framework/Utils/EthAccount.cs | 27 ++- Framework/Utils/EthAddress.cs | 20 +- .../Marketplace/Customizations.cs | 6 + .../MarketTests/ContractFailedTest.cs | 28 --- .../MarketTests/ContractRepairedTest.cs | 18 -- .../MarketplaceAutoBootstrapDistTest.cs | 54 ++++- .../MarketTests/RepairTest.cs | 185 ++++++++++++++++++ .../Utils/EthAccountEqualityTests.cs | 31 +++ 8 files changed, 317 insertions(+), 52 deletions(-) delete mode 100644 Tests/CodexReleaseTests/MarketTests/ContractRepairedTest.cs create mode 100644 Tests/CodexReleaseTests/MarketTests/RepairTest.cs create mode 100644 Tests/FrameworkTests/Utils/EthAccountEqualityTests.cs diff --git a/Framework/Utils/EthAccount.cs b/Framework/Utils/EthAccount.cs index 0898a30a..6119eb4e 100644 --- a/Framework/Utils/EthAccount.cs +++ b/Framework/Utils/EthAccount.cs @@ -1,7 +1,7 @@ namespace Utils { [Serializable] - public class EthAccount + public class EthAccount : IComparable { public EthAccount(EthAddress ethAddress, string privateKey) { @@ -12,9 +12,34 @@ public EthAddress EthAddress { get; } public string PrivateKey { get; } + public int CompareTo(EthAccount? other) + { + return PrivateKey.CompareTo(other!.PrivateKey); + } + + public override bool Equals(object? obj) + { + return obj is EthAccount token && PrivateKey == token.PrivateKey; + } + + public override int GetHashCode() + { + return HashCode.Combine(PrivateKey); + } + public override string ToString() { return EthAddress.ToString(); } + + public static bool operator ==(EthAccount a, EthAccount b) + { + return a.PrivateKey == b.PrivateKey; + } + + public static bool operator !=(EthAccount a, EthAccount b) + { + return a.PrivateKey != b.PrivateKey; + } } } diff --git a/Framework/Utils/EthAddress.cs b/Framework/Utils/EthAddress.cs index 61c2776c..7ac80093 100644 --- a/Framework/Utils/EthAddress.cs +++ b/Framework/Utils/EthAddress.cs @@ -6,7 +6,7 @@ } [Serializable] - public class EthAddress + public class EthAddress : IComparable { public EthAddress(string address) { @@ -15,10 +15,14 @@ public string Address { get; } + public int CompareTo(EthAddress? other) + { + return Address.CompareTo(other!.Address); + } + public override bool Equals(object? obj) { - return obj is EthAddress address && - Address == address.Address; + return obj is EthAddress token && Address == token.Address; } public override int GetHashCode() @@ -30,5 +34,15 @@ { return Address; } + + public static bool operator ==(EthAddress a, EthAddress b) + { + return a.Address == b.Address; + } + + public static bool operator !=(EthAddress a, EthAddress b) + { + return a.Address != b.Address; + } } } diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index 222dc631..c69dab8b 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -1,5 +1,6 @@ #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. using BlockchainUtils; +using Nethereum.Hex.HexConvertors.Extensions; using Newtonsoft.Json; using Utils; @@ -61,6 +62,11 @@ namespace CodexContractsPlugin.Marketplace [JsonIgnore] public BlockTimeEntry Block { get; set; } public EthAddress Host { get; set; } + + public override string ToString() + { + return $"SlotFilled:[host:{Host} request:{RequestId.ToHex()} slotIndex:{SlotIndex}]"; + } } public partial class SlotFreedEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex diff --git a/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs index caeaed9b..d340ba26 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs @@ -1,5 +1,4 @@ using CodexClient; -using CodexContractsPlugin.Marketplace; using NUnit.Framework; using Utils; @@ -57,33 +56,6 @@ namespace CodexReleaseTests.MarketTests Assert.Fail($"{nameof(WaitForSlotFreedEvents)} failed after {Time.FormatDuration(timeout)}"); } - private TimeSpan CalculateContractFailTimespan() - { - var config = GetContracts().Deployment.Config; - var requiredNumMissedProofs = Convert.ToInt32(config.Collateral.MaxNumberOfSlashes); - var periodDuration = GetPeriodDuration(); - - // 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 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; - } - private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { var cid = client.UploadFile(GenerateTestFile(5.MB())); diff --git a/Tests/CodexReleaseTests/MarketTests/ContractRepairedTest.cs b/Tests/CodexReleaseTests/MarketTests/ContractRepairedTest.cs deleted file mode 100644 index 752afe18..00000000 --- a/Tests/CodexReleaseTests/MarketTests/ContractRepairedTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CodexReleaseTests.MarketTests -{ - public class ContractRepairedTest - { - [Test] - [Ignore("TODO - Test in which a host fails, but the slot is repaired")] - public void ContractRepaired() - { - } - } -} diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index c14222df..74a1b6d5 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -80,6 +80,29 @@ namespace CodexReleaseTests.MarketTests return hosts; } + 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) { AssertTstBalance(node.EthAddress, expectedBalance, message); @@ -185,7 +208,7 @@ namespace CodexReleaseTests.MarketTests ); } - public SlotFill[] GetOnChainSlotFills(ICodexNodeGroup possibleHosts, string purchaseId) + public SlotFill[] GetOnChainSlotFills(IEnumerable possibleHosts, string purchaseId) { var fills = GetOnChainSlotFills(possibleHosts); return fills.Where(f => f @@ -193,7 +216,7 @@ namespace CodexReleaseTests.MarketTests .ToArray(); } - public SlotFill[] GetOnChainSlotFills(ICodexNodeGroup possibleHosts) + public SlotFill[] GetOnChainSlotFills(IEnumerable possibleHosts) { var events = GetContracts().GetEvents(GetTestRunTimeRange()); var fills = events.GetSlotFilledEvents(); @@ -356,6 +379,33 @@ namespace CodexReleaseTests.MarketTests }, description); } + 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; + } public class SlotFill { public SlotFill(SlotFilledEventDTO slotFilledEvent, ICodexNode host) diff --git a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs new file mode 100644 index 00000000..6ae53a32 --- /dev/null +++ b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs @@ -0,0 +1,185 @@ +using CodexClient; +using Nethereum.Hex.HexConvertors.Extensions; +using NUnit.Framework; +using Utils; + +namespace CodexReleaseTests.MarketTests +{ + [TestFixture] + public class RepairTest : MarketplaceAutoBootstrapDistTest + { + #region Setup + + private readonly ByteSize Filesize; + private readonly uint Slots; + private readonly uint Tolerance; + private readonly ByteSize EncodedFilesize; + private readonly ByteSize SlotSize; + + public RepairTest() + { + Filesize = 32.MB(); + Slots = 4; + Tolerance = 2; + + EncodedFilesize = new ByteSize(Filesize.SizeInBytes * (Slots / Tolerance)); + SlotSize = new ByteSize(EncodedFilesize.SizeInBytes / Slots); + Assert.That(IsPowerOfTwo(SlotSize)); + Assert.That(Slots, Is.LessThan(NumberOfHosts)); + } + + protected override int NumberOfHosts => 5; + protected override int NumberOfClients => 1; + protected override ByteSize HostAvailabilitySize => SlotSize.Multiply(1.1); // Each host can hold 1 slot. + protected override TimeSpan HostAvailabilityMaxDuration => GetPeriodDuration() * 100; + + private static bool IsPowerOfTwo(ByteSize size) + { + var x = size.SizeInBytes; + return (x != 0) && ((x & (x - 1)) == 0); + } + + #endregion + + [Test] + [Combinatorial] + public void RollingRepairSingleFailure( + [Values(10)] int numFailures) + { + var hosts = StartHosts().ToList(); + var client = StartClients().Single(); + + 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 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) + { + var cid = client.UploadFile(GenerateTestFile(Filesize)); + var config = GetContracts().Deployment.Config; + return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) + { + Duration = TimeSpan.FromDays(2.0), + Expiry = TimeSpan.FromMinutes(10.0), + MinRequiredNumberOfNodes = Slots, + NodeFailureTolerance = Tolerance, + PricePerBytePerSecond = 10.TstWei(), + ProofProbability = 20, + CollateralPerByte = 1.TstWei() + }); + } + } +} diff --git a/Tests/FrameworkTests/Utils/EthAccountEqualityTests.cs b/Tests/FrameworkTests/Utils/EthAccountEqualityTests.cs new file mode 100644 index 00000000..337516ae --- /dev/null +++ b/Tests/FrameworkTests/Utils/EthAccountEqualityTests.cs @@ -0,0 +1,31 @@ +using GethPlugin; +using NUnit.Framework; + +namespace FrameworkTests.Utils +{ + [TestFixture] + public class EthAccountEqualityTests + { + [Test] + public void Accounts() + { + var account1 = EthAccountGenerator.GenerateNew(); + var account2 = EthAccountGenerator.GenerateNew(); + + Assert.That(account1, Is.EqualTo(account1)); + Assert.That(account1 == account1); + Assert.That(account1 != account2); + } + + [Test] + public void Addresses() + { + var address1 = EthAccountGenerator.GenerateNew().EthAddress; + var address2 = EthAccountGenerator.GenerateNew().EthAddress; + + Assert.That(address1, Is.EqualTo(address1)); + Assert.That(address1 == address1); + Assert.That(address1 != address2); + } + } +} From 07d39ef1393b1f2f588b2c5033162f8898761f43 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 17:06:44 +0200 Subject: [PATCH 60/69] Fixes repair test. Disables until implemented --- Framework/Utils/EthAccount.cs | 9 ++++++--- Framework/Utils/EthAddress.cs | 9 ++++++--- Tests/CodexReleaseTests/MarketTests/RepairTest.cs | 5 +++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Framework/Utils/EthAccount.cs b/Framework/Utils/EthAccount.cs index 6119eb4e..018e43fc 100644 --- a/Framework/Utils/EthAccount.cs +++ b/Framework/Utils/EthAccount.cs @@ -32,14 +32,17 @@ return EthAddress.ToString(); } - public static bool operator ==(EthAccount a, EthAccount b) + public static bool operator ==(EthAccount? a, EthAccount? b) { + if (ReferenceEquals(a, b)) return true; + if (ReferenceEquals(a, null)) return false; + if (ReferenceEquals(b, null)) return false; return a.PrivateKey == b.PrivateKey; } - public static bool operator !=(EthAccount a, EthAccount b) + public static bool operator !=(EthAccount? a, EthAccount? b) { - return a.PrivateKey != b.PrivateKey; + return !(a == b); } } } diff --git a/Framework/Utils/EthAddress.cs b/Framework/Utils/EthAddress.cs index 7ac80093..88080fb9 100644 --- a/Framework/Utils/EthAddress.cs +++ b/Framework/Utils/EthAddress.cs @@ -35,14 +35,17 @@ return Address; } - public static bool operator ==(EthAddress a, EthAddress b) + public static bool operator ==(EthAddress? a, EthAddress? b) { + if (ReferenceEquals(a, b)) return true; + if (ReferenceEquals(a, null)) return false; + if (ReferenceEquals(b, null)) return false; return a.Address == b.Address; } - public static bool operator !=(EthAddress a, EthAddress b) + public static bool operator !=(EthAddress? a, EthAddress? b) { - return a.Address != b.Address; + return !(a == b); } } } diff --git a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs index 6ae53a32..7d9bdfed 100644 --- a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs @@ -31,7 +31,7 @@ namespace CodexReleaseTests.MarketTests protected override int NumberOfHosts => 5; protected override int NumberOfClients => 1; protected override ByteSize HostAvailabilitySize => SlotSize.Multiply(1.1); // Each host can hold 1 slot. - protected override TimeSpan HostAvailabilityMaxDuration => GetPeriodDuration() * 100; + protected override TimeSpan HostAvailabilityMaxDuration => TimeSpan.FromDays(5.0); private static bool IsPowerOfTwo(ByteSize size) { @@ -41,6 +41,7 @@ namespace CodexReleaseTests.MarketTests #endregion + [Ignore("Test is ready. Waiting for repair implementation.")] [Test] [Combinatorial] public void RollingRepairSingleFailure( @@ -172,7 +173,7 @@ namespace CodexReleaseTests.MarketTests var config = GetContracts().Deployment.Config; return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { - Duration = TimeSpan.FromDays(2.0), + Duration = HostAvailabilityMaxDuration / 2, Expiry = TimeSpan.FromMinutes(10.0), MinRequiredNumberOfNodes = Slots, NodeFailureTolerance = Tolerance, From a722f900f4a12d38a59f69b508b9b84ec8a641cc Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 18:36:37 +0200 Subject: [PATCH 61/69] Reduces proof requirement for generations test --- Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs index aae63ecb..b37a9af2 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -88,7 +88,7 @@ namespace CodexReleaseTests.MarketTests MinRequiredNumberOfNodes = (uint)slots, NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, - ProofProbability = 1, + ProofProbability = 1000, CollateralPerByte = 1.TstWei() }); } From 4a863426334170ab216b40138b32f5ea8aad142d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Thu, 29 May 2025 19:10:11 +0200 Subject: [PATCH 62/69] Jumps to latest release candidate --- ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs | 2 +- ProjectPlugins/CodexPlugin/CodexDockerImage.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs index e46bbdd9..dba71566 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs @@ -15,7 +15,7 @@ namespace CodexContractsPlugin.Marketplace public class MarketplaceDeploymentBase : ContractDeploymentMessage { - public static string BYTECODE = "0x60c060405234801561001057600080fd5b50604051614f0a380380614f0a83398101604081905261002f9161053b565b602083015180516040850151516001805460ff191660ff90921691909117905582906001600160401b03811660000361007b5760405163015536c760e51b815260040160405180910390fd5b6001600160401b031660805261010043116100a9576040516338f5f66160e11b815260040160405180910390fd5b8151600280546020850151604086015160608701516001600160401b039586166001600160801b0319909416939093176801000000000000000095909216949094021761ffff60801b1916600160801b60ff9485160260ff60881b191617600160881b9390911692909202919091178155608083015183919060039061012f90826106d9565b5050600480546001600160a01b0319166001600160a01b0393841617905550831660a05250825151606460ff909116111561017d576040516302bd816360e41b815260040160405180910390fd5b606483600001516040015160ff1611156101aa576040516354e5e0ab60e11b815260040160405180910390fd5b825160408101516020909101516064916101c391610797565b60ff1611156101e5576040516317ff9d0f60e21b815260040160405180910390fd5b82518051600b805460208085015160408087015160609788015160ff90811663010000000263ff0000001992821662010000029290921663ffff0000199482166101000261ffff1990971698821698909817959095179290921695909517178355808801518051600c80549383015196830151978301518516600160881b0260ff60881b1998909516600160801b029790971661ffff60801b196001600160401b0397881668010000000000000000026001600160801b031990951697909216969096179290921791909116939093171783556080820151869391929190600d906102d090826106d9565b50505060408201515160038201805460ff191660ff909216919091179055606090910151600490910180546001600160401b0319166001600160401b03909216919091179055506107c8915050565b634e487b7160e01b600052604160045260246000fd5b60405160a081016001600160401b03811182821017156103575761035761031f565b60405290565b604051608081016001600160401b03811182821017156103575761035761031f565b604051601f8201601f191681016001600160401b03811182821017156103a7576103a761031f565b604052919050565b805160ff811681146103c057600080fd5b919050565b80516001600160401b03811681146103c057600080fd5b600060a082840312156103ee57600080fd5b6103f6610335565b9050610401826103c5565b815261040f602083016103c5565b6020820152610420604083016103af565b6040820152610431606083016103af565b606082015260808201516001600160401b0381111561044f57600080fd5b8201601f8101841361046057600080fd5b80516001600160401b038111156104795761047961031f565b61048c601f8201601f191660200161037f565b8181528560208385010111156104a157600080fd5b60005b828110156104c0576020818501810151838301820152016104a4565b5060006020838301015280608085015250505092915050565b6000602082840312156104eb57600080fd5b604051602081016001600160401b038111828210171561050d5761050d61031f565b60405290508061051c836103af565b905292915050565b80516001600160a01b03811681146103c057600080fd5b60008060006060848603121561055057600080fd5b83516001600160401b0381111561056657600080fd5b840180860360e081121561057957600080fd5b61058161035d565b608082121561058f57600080fd5b61059761035d565b91506105a2836103af565b82526105b0602084016103af565b60208301526105c1604084016103af565b60408301526105d2606084016103af565b60608301529081526080820151906001600160401b038211156105f457600080fd5b610600888385016103dc565b60208201526106128860a085016104d9565b604082015261062360c084016103c5565b6060820152945061063991505060208501610524565b915061064760408501610524565b90509250925092565b600181811c9082168061066457607f821691505b60208210810361068457634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156106d457806000526020600020601f840160051c810160208510156106b15750805b601f840160051c820191505b818110156106d157600081556001016106bd565b50505b505050565b81516001600160401b038111156106f2576106f261031f565b610706816107008454610650565b8461068a565b6020601f82116001811461073a57600083156107225750848201515b600019600385901b1c1916600184901b1784556106d1565b600084815260208120601f198516915b8281101561076a578785015182556020948501946001909201910161074a565b50848210156107885786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b60ff81811683821602908116908181146107c157634e487b7160e01b600052601160045260246000fd5b5092915050565b60805160a0516146e5610825600039600081816104bf01528181610f7001528181612019015281816125d30152818161268301528181612812015281816128c20152612cec01526000818161355601526137e601526146e56000f3fe608060405234801561001057600080fd5b50600436106101c45760003560e01c80636e2b54ee116100f9578063c0cc4add11610097578063e8aa0a0711610071578063e8aa0a0714610461578063f752196b14610474578063fb1e61ca1461049d578063fc0c546a146104bd57600080fd5b8063c0cc4add14610428578063c5d433511461043b578063d02bbe331461044e57600080fd5b8063a29c29a4116100d3578063a29c29a4146103b2578063a3a0807e146103c5578063b396dc79146103e8578063be5cdc481461040857600080fd5b80636e2b54ee146103845780639777b72c1461039757806399b6da0c1461039f57600080fd5b8063329b5a0b1161016657806351a766421161014057806351a76642146103035780635da73835146103165780636b00c8cf1461032b5780636c70bee91461036f57600080fd5b8063329b5a0b14610298578063458d2bf1146102cb5780634641dce6146102de57600080fd5b806312827602116101a2578063128276021461022e5780631d873c1b14610241578063237d84821461025457806326d6f8341461026757600080fd5b806302fa8e65146101c957806305b90773146101f95780630aefaabe14610219575b600080fd5b6101dc6101d73660046138b3565b6104e3565b6040516001600160401b0390911681526020015b60405180910390f35b61020c6102073660046138b3565b6105c1565b6040516101f091906138e2565b61022c610227366004613911565b6106e4565b005b61022c61023c366004613978565b610877565b61022c61024f3660046139bb565b610948565b61022c610262366004613978565b610dfa565b61028a6102753660046138b3565b60009081526012602052604090206003015490565b6040519081526020016101f0565b6101dc6102a63660046138b3565b600090815260116020526040902060020154600160c01b90046001600160401b031690565b61028a6102d93660046138b3565b611046565b6102f16102ec3660046138b3565b61105f565b60405160ff90911681526020016101f0565b61028a6103113660046138b3565b611072565b61031e6110d1565b6040516101f091906139fb565b6103576103393660046138b3565b6000908152601260205260409020600401546001600160a01b031690565b6040516001600160a01b0390911681526020016101f0565b6103776110f8565b6040516101f09190613ad9565b61022c6103923660046138b3565b61126f565b61031e61127c565b61022c6103ad366004613b61565b61129b565b61022c6103c03660046138b3565b6117e1565b6103d86103d33660046138b3565b611833565b60405190151581526020016101f0565b6103fb6103f63660046138b3565b61186f565b6040516101f09190613c90565b61041b6104163660046138b3565b611b51565b6040516101f09190613ccb565b6103d86104363660046138b3565b611c1f565b61022c610449366004613cdf565b611c32565b6103d861045c366004613978565b6120b3565b61022c61046f366004613d04565b61211f565b6101dc6104823660046138b3565b6000908152600660205260409020546001600160401b031690565b6104b06104ab3660046138b3565b612298565b6040516101f09190613d32565b7f0000000000000000000000000000000000000000000000000000000000000000610357565b6000806104ef836105c1565b90506000816004811115610505576105056138cc565b148061052257506001816004811115610520576105206138cc565b145b1561054e575050600090815260116020526040902060020154600160801b90046001600160401b031690565b6002816004811115610562576105626138cc565b0361058e575050600090815260116020526040902060020154600160c01b90046001600160401b031690565b6000838152601160205260409020600201546105ba90600160801b90046001600160401b0316426124ae565b9392505050565b60008181526010602052604081205482906001600160a01b03166105f857604051635eeb253d60e11b815260040160405180910390fd5b600083815260116020526040812090815460ff16600481111561061d5761061d6138cc565b14801561065c5750600084815260116020526040902060020154600160c01b90046001600160401b03166001600160401b0316426001600160401b0316115b1561066b5760029250506106de565b6001815460ff166004811115610683576106836138cc565b14806106a457506000815460ff1660048111156106a2576106a26138cc565b145b80156106c8575060028101546001600160401b03600160801b909104811642909116115b156106d75760039250506106de565b5460ff1691505b50919050565b826000808281526012602052604090205460ff166006811115610709576107096138cc565b0361072757604051638b41ec7f60e01b815260040160405180910390fd5b600084815260126020526040902060048101546001600160a01b0316331461077b576040517f57a6f4e900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600061078686611b51565b9050600481600681111561079c5761079c6138cc565b036107d3576040517fc2cbf77700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60028160068111156107e7576107e76138cc565b03610801576107fc82600101548787876124c4565b61086f565b6005816006811115610815576108156138cc565b0361082a576107fc826001015487878761270d565b600381600681111561083e5761083e6138cc565b0361084d576107fc3387612956565b6001816006811115610861576108616138cc565b0361086f5761086f86612978565b505050505050565b61088182826120b3565b6108b7576040517f424a04ab00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006108c38383612bd0565b60008181526020819052604090209091506108de9033612c15565b50600154600082815260208190526040902060ff909116906108ff90612c2a565b03610943576040516001600160401b038316815283907fc8e6c955744189a19222ec226b72ac1435d88d5745252dac56e6f679f64c037a9060200160405180910390a25b505050565b60008381526010602052604090205483906001600160a01b031661097f57604051635eeb253d60e11b815260040160405180910390fd5b600084815260106020526040902060048101546001600160401b03908116908516106109d7576040517f3b920b8800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006109e38686612bd0565b60008181526020819052604090209091506109fe9033612c34565b610a34576040517fd651ce1800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000818152601260209081526040808320600181018a90556002810180546fffffffffffffffff00000000000000001916600160401b6001600160401b038c1602179055898452601190925282209091610a8d84611b51565b6006811115610a9e57610a9e6138cc565b14158015610ac657506006610ab284611b51565b6006811115610ac357610ac36138cc565b14155b15610afd576040517fff556acf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60048201805473ffffffffffffffffffffffffffffffffffffffff1916331790556002820180546001600160401b03421667ffffffffffffffff19909116179055610b6e83600090815260056020526040902080546001600160401b03421667ffffffffffffffff19909116179055565b610b78838761211f565b60028101805460019190600090610b999084906001600160401b0316613d5b565b92506101000a8154816001600160401b0302191690836001600160401b03160217905550610bde888360020160009054906101000a90046001600160401b0316612c56565b816001016000828254610bf19190613d7a565b90915550506040805160e081018252600186015481526002860154602082015260038601549181019190915260048501546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c08201526000908190610c6a90612c85565b90506006610c7786611b51565b6006811115610c8857610c886138cc565b03610cbb57600b54606490610ca09060ff1683613d8d565b610caa9190613dba565b610cb49082613d7a565b9150610cbf565b8091505b610cc93383612ca4565b8160136000016000828254610cde9190613dce565b9091555050600384018190556004840154610d02906001600160a01b031686612d78565b835460ff191660011784556040516001600160401b038a1681528a907f8f301470a994578b52323d625dfbf827ca5208c81747d3459be7b8867baec3ec9060200160405180910390a2600486015460028401546001600160401b039081169116148015610d8457506000835460ff166004811115610d8257610d826138cc565b145b15610dee57825460ff191660011783556002830180546001600160401b034216600160401b026fffffffffffffffff0000000000000000199091161790556040518a907f85e1543bf2f84fe80c6badbce3648c8539ad1df4d2b3d822938ca0538be727e690600090a25b50505050505050505050565b6001610e0583611b51565b6006811115610e1657610e166138cc565b14610e4d576040517fae9dcffd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610e578282612d9a565b6000828152601260209081526040808320600180820154855260108452828520600b54845160e08101865292820154835260028201549583019590955260038101549382019390935260048301546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c0820152909391926064916201000090910460ff1690610efa90612c85565b610f049190613d8d565b610f0e9190613dba565b600b54909150600090606490610f2e906301000000900460ff1684613d8d565b610f389190613dba565b90508060136001016000828254610f4f9190613dce565b909155505060405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303816000875af1158015610fc1573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610fe59190613de1565b610ff157610ff1613e03565b818460030160008282546110059190613d7a565b9091555050600b5460008781526006602052604090205461010090910460ff16906001600160401b03166001600160401b03161061086f5761086f86612978565b600061105982611054612fb1565b612fbc565b92915050565b60006110598261106d612fb1565b612fd0565b60008181526012602090815260408083206001810154845260109092528220600c54610100906110ac90600160801b900460ff1682613e19565b60018301546110bf9161ffff1690613d8d565b6110c99190613dba565b949350505050565b336000908152600a602052604090206060906110f3906110f090613062565b90565b905090565b61110061380b565b604080516101008082018352600b805460ff8082166080808701918252948304821660a080880191909152620100008404831660c08801526301000000909304821660e0870152855285519182018652600c80546001600160401b038082168552600160401b820416602085810191909152600160801b82048416988501989098527101000000000000000000000000000000000090049091166060830152600d80549596939593870194929391928401916111bb90613e33565b80601f01602080910402602001604051908101604052809291908181526020018280546111e790613e33565b80156112345780601f1061120957610100808354040283529160200191611234565b820191906000526020600020905b81548152906001019060200180831161121757829003601f168201915b5050509190925250505081526040805160208181018352600385015460ff1682528301526004909201546001600160401b0316910152919050565b6112798133611c32565b50565b3360009081526009602052604090206060906110f3906110f090613062565b60006112ae6112a983613fc4565b61306f565b9050336112be60208401846140cd565b6001600160a01b0316146112e5576040516334c69e3160e11b815260040160405180910390fd5b6000818152601060205260409020546001600160a01b031615611334576040517ffc7d069000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b611346610140830161012084016140ea565b6001600160401b0316158061138d575061136660e0830160c084016140ea565b6001600160401b0316611381610140840161012085016140ea565b6001600160401b031610155b156113c4576040517fdf63f61a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6113d460a08301608084016140ea565b6001600160401b0316600003611416576040517f535ed2be00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61142660a08301608084016140ea565b6001600160401b0316611440610100840160e085016140ea565b6001600160401b03161115611481576040517fb9551ab100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61149160e0830160c084016140ea565b6001600160401b03166000036114d3576040517f090a5ecd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6020820135600003611511576040517f6aba7aae00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606082013560000361154f576040517ffb7df0c700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604082013560000361158d576040517f47ba51c700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61159b610100830183614107565b6115a59080614127565b90506000036115e0576040517f86f8cf9b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600f546001600160401b03166115fc60e0840160c085016140ea565b6001600160401b0316111561163d576040517f1267b3f200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000818152601060205260409020829061165782826142c7565b5061166a905060e0830160c084016140ea565b6116749042613d5b565b600082815260116020526040902060020180546001600160401b0392909216600160801b0267ffffffffffffffff60801b199092169190911790556116c1610140830161012084016140ea565b6116cb9042613d5b565b600082815260116020908152604090912060020180546001600160401b0393909316600160c01b0277ffffffffffffffffffffffffffffffffffffffffffffffff9093169290921790915561172c90611726908401846140cd565b8261309f565b600061173f61173a84613fc4565b6130c1565b600083815260116020526040812060010182905560138054929350839290919061176a908490613dce565b9091555061177a90503382612ca4565b6000828152601160209081526040918290206002015491517f1bf9c457accf8703dbf7cdf1b58c2f74ddf2e525f98155c70b3d318d74609bd8926117d492869290880191600160c01b90046001600160401b03169061447b565b60405180910390a1505050565b806000808281526012602052604090205460ff166006811115611806576118066138cc565b0361182457604051638b41ec7f60e01b815260040160405180910390fd5b61182f8233336106e4565b5050565b600080600061184984611844612fb1565b6130fd565b90925090508180156110c95750600254600160801b900460ff9081169116109392505050565b6118f260405180604001604052806138746040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b816000808281526012602052604090205460ff166006811115611917576119176138cc565b0361193557604051638b41ec7f60e01b815260040160405180910390fd5b60008381526012602052604090206119c660405180604001604052806138746040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b600180830154600090815260106020908152604091829020825160a0808201855282546001600160a01b03168252845160e08101865295830154865260028301548685015260038301548686015260048301546001600160401b038082166060890152600160401b820481166080890152600160801b8204811692880192909252600160c01b90041660c0860152918201939093528151808301835260058401805492949385019282908290611a7b90613e33565b80601f0160208091040260200160405190810160405280929190818152602001828054611aa790613e33565b8015611af45780601f10611ac957610100808354040283529160200191611af4565b820191906000526020600020905b815481529060010190602001808311611ad757829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0390811683830152600890930154604090920191909152918352600290930154600160401b900490921691810191909152915050919050565b600081815260126020526040812060018101548203611b735750600092915050565b6000611b8282600101546105c1565b90506004825460ff166006811115611b9c57611b9c6138cc565b03611bab575060049392505050565b6002816004811115611bbf57611bbf6138cc565b03611bce575060059392505050565b6003816004811115611be257611be26138cc565b03611bf1575060029392505050565b6004816004811115611c0557611c056138cc565b03611c14575060039392505050565b505460ff1692915050565b600061105982611c2d612fb1565b6131b5565b60008281526010602052604090205482906001600160a01b0316611c6957604051635eeb253d60e11b815260040160405180910390fd5b6000838152601060209081526040808320601190925290912081546001600160a01b03163314611cac576040516334c69e3160e11b815260040160405180910390fd5b6000611cb7866105c1565b90506002816004811115611ccd57611ccd6138cc565b14158015611ced57506004816004811115611cea57611cea6138cc565b14155b8015611d0b57506003816004811115611d0857611d086138cc565b14155b15611d42576040517fc00b5b5700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8160010154600003611d80576040517fbd8bdd9400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6002816004811115611d9457611d946138cc565b03611e3257815460ff1916600217825560405186907ff903f4774c7bd27355f9d7fcbc382b079b164a697a44ac5d95267a4c3cb3bb2290600090a2600086815260116020526040902060020154611dfc908790600160c01b90046001600160401b0316612c56565b6002830154611e1491906001600160401b0316613d8d565b826001016000828254611e279190613dce565b90915550611fbf9050565b6004816004811115611e4657611e466138cc565b03611fb3576040805160a0808201835285546001600160a01b03168252825160e08101845260018701548152600287015460208281019190915260038801548286015260048801546001600160401b038082166060850152600160401b820481166080850152600160801b8204811694840194909452600160c01b900490921660c08201529082015281518083018352600586018054611fa994889390850192909182908290611ef590613e33565b80601f0160208091040260200160405190810160405280929190818152602001828054611f2190613e33565b8015611f6e5780601f10611f4357610100808354040283529160200191611f6e565b820191906000526020600020905b815481529060010190602001808311611f5157829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0316908201526008909101546040909101526130c1565b6001830155611fbf565b815460ff191660031782555b8254611fd4906001600160a01b0316876131ec565b600182015460148054829190600090611fee908490613dce565b909155505060405163a9059cbb60e01b81526001600160a01b038781166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af1158015612062573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120869190613de1565b6120a357604051637c2ccffd60e11b815260040160405180910390fd5b5050600060019091015550505050565b600033816120c18585612bd0565b90506120cc8161320e565b80156120f55750600154600082815260208190526040902060ff909116906120f390612c2a565b105b8015612116575060008181526020819052604090206121149083612c34565b155b95945050505050565b6000828152601260209081526040808320600101548084526010909252909120546001600160a01b031661216657604051635eeb253d60e11b815260040160405180910390fd5b600083815260126020526040902060048101546001600160a01b031633146121ba576040517fce351b9400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600181015460009081526010602052604080822081516003808252608082019093529092918160200160208202803683370190505090506122026121fd87611046565b61323b565b8160008151811061221557612215614528565b6020908102919091010152600682015461222e9061324c565b8160018151811061224157612241614528565b6020026020010181815250508260020160089054906101000a90046001600160401b03166001600160401b03168160028151811061228157612281614528565b60200260200101818152505061086f868683613258565b61230d6040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b60008281526010602052604090205482906001600160a01b031661234457604051635eeb253d60e11b815260040160405180910390fd5b600083815260106020908152604091829020825160a0808201855282546001600160a01b03168252845160e0810186526001840154815260028401548186015260038401548187015260048401546001600160401b038082166060840152600160401b820481166080840152600160801b8204811693830193909352600160c01b900490911660c082015292810192909252825180840184526005820180549394929392850192829082906123f890613e33565b80601f016020809104026020016040519081016040528092919081815260200182805461242490613e33565b80156124715780601f1061244657610100808354040283529160200191612471565b820191906000526020600020905b81548152906001019060200180831161245457829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0316908201526008909101546040909101529392505050565b60008183106124bd57816105ba565b5090919050565b60008481526010602052604090205484906001600160a01b03166124fb57604051635eeb253d60e11b815260040160405180910390fd5b600085815260116020908152604080832060108352818420815460ff1916600317825588855260129093529220815461253d906001600160a01b0316896131ec565b6004810154612555906001600160a01b031688612956565b6002810154600090612571908a906001600160401b0316612c56565b60038301549091506125838183613dce565b60148054600090612595908490613dce565b90915550508254600490849060ff1916600183021790555060405163a9059cbb60e01b81526001600160a01b038981166004830152602482018490527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561261c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906126409190613de1565b61265d57604051637c2ccffd60e11b815260040160405180910390fd5b60405163a9059cbb60e01b81526001600160a01b038881166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af11580156126cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906126f09190613de1565b610dee57604051637c2ccffd60e11b815260040160405180910390fd5b60008481526010602052604090205484906001600160a01b031661274457604051635eeb253d60e11b815260040160405180910390fd5b6000848152601260205260409020600481015461276a906001600160a01b031686612956565b60028101546000906127b09088906001600160401b03166127ab826000908152601160205260409020600201546001600160401b03600160c01b9091041690565b6133f6565b60038301549091506127c28183613dce565b601480546000906127d4908490613dce565b90915550508254600490849060ff1916600183021790555060405163a9059cbb60e01b81526001600160a01b038781166004830152602482018490527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561285b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061287f9190613de1565b61289c57604051637c2ccffd60e11b815260040160405180910390fd5b60405163a9059cbb60e01b81526001600160a01b038681166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561290b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061292f9190613de1565b61294c57604051637c2ccffd60e11b815260040160405180910390fd5b5050505050505050565b6001600160a01b0382166000908152600a6020526040902061094390826134d5565b600081815260126020908152604080832060018101548085526011909352922060028301546129b19083906001600160401b0316612c56565b8160010160008282546129c49190613dce565b909155505060048301546129e1906001600160a01b031685612956565b60008481526020819052604081209081816129fc8282613881565b5050845460ff1916600617855550506002808401805467ffffffffffffffff1916905560006003850181905560048501805473ffffffffffffffffffffffffffffffffffffffff19169055908201805460019290612a649084906001600160401b031661453e565b82546101009290920a6001600160401b038181021990931691831602179091556002850154604051600160401b90910490911681528391507f33ba8f7627565d89f7ada2a6b81ea532b7aa9b11e91a78312d6e1fca0bfcd1dc9060200160405180910390a26000848152600660205260409020805467ffffffffffffffff19169055600082815260106020526040812060028301546004820154919291612b17916001600160401b03908116911661453e565b60048301546001600160401b039182169250600160c01b90041681118015612b5457506001835460ff166004811115612b5257612b526138cc565b145b1561086f57825460ff19166004178355612b6f60014261453e565b6002840180546001600160401b0392909216600160801b0267ffffffffffffffff60801b1990921691909117905560405184907f4769361a442504ecaf038f35e119bcccdd5e42096b24c09e3c17fd17c6684c0290600090a2505050505050565b60008282604051602001612bf79291909182526001600160401b0316602082015260400190565b60405160208183030381529060405280519060200120905092915050565b60006105ba836001600160a01b0384166134e1565b6000611059825490565b6001600160a01b038116600090815260018301602052604081205415156105ba565b6000828152601160205260408120600201546105ba9084908490600160801b90046001600160401b03166133f6565b600081608001516001600160401b031682604001516110599190613d8d565b6040517f23b872dd0000000000000000000000000000000000000000000000000000000081526001600160a01b038381166004830152306024830181905260448301849052917f0000000000000000000000000000000000000000000000000000000000000000909116906323b872dd906064016020604051808303816000875af1158015612d37573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612d5b9190613de1565b61094357604051637c2ccffd60e11b815260040160405180910390fd5b6001600160a01b0382166000908152600a602052604090206109439082613530565b6000612da58261353c565b6001600160401b03169050428110612de9576040517f6b4b1a4e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600254612e0690600160401b90046001600160401b031682613dce565b4210612e3e576040517fde55698e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526007602090815260408083206001600160401b038616845290915290205460ff1615612e9a576040517efab7d900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b612ea483836131b5565b612eda576040517fd3ffa66b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526008602090815260408083206001600160401b038616845290915290205460ff1615612f37576040517f98e7e55100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526008602090815260408083206001600160401b038087168552908352818420805460ff1916600190811790915587855260069093529083208054929390929091612f8891859116613d5b565b92506101000a8154816001600160401b0302191690836001600160401b03160217905550505050565b60006110f34261354f565b60006105ba612fcb8484612fd0565b61357b565b600080612fdf6101004361455d565b6002549091506000906101009061300e9071010000000000000000000000000000000000900460ff1686614571565b6130189190614593565b6001600160401b0316905060006130316101008761455d565b90506000610100826130438587613dce565b61304d9190613dce565b613057919061455d565b979650505050505050565b606060006105ba836135d5565b6000816040516020016130829190613d32565b604051602081830303815290604052805190602001209050919050565b6001600160a01b03821660009081526009602052604090206109439082613530565b60006130d08260200151613631565b602083015160a08101516060909101516130ea9190614571565b6001600160401b03166110599190613d8d565b600080600061310b85611b51565b60008681526005602052604081205491925090613130906001600160401b031661354f565b90506001826006811115613146576131466138cc565b14158061315a57506131588582613650565b155b1561316d576000809350935050506131ae565b6131778686612fd0565b925060006131848461357b565b9050600061319188611072565b90508015806131a757506131a5818361455d565b155b9550505050505b9250929050565b60008060006131c485856130fd565b90925090508180156121165750600254600160801b900460ff90811691161015949350505050565b6001600160a01b038216600090815260096020526040902061094390826134d5565b60008060008381526012602052604090205460ff166006811115613234576132346138cc565b1492915050565b600060ff198216816110c982613666565b6000806105ba83613666565b60008381526007602052604081209061326f612fb1565b6001600160401b0316815260208101919091526040016000205460ff16156132c3576040517f3edef7db00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600480546040517f94c8919d0000000000000000000000000000000000000000000000000000000081526001600160a01b03909116916394c8919d9161330d9186918691016145c1565b602060405180830381865afa15801561332a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061334e9190613de1565b613384576040517ffcd03a4700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600083815260076020526040812060019161339d612fb1565b6001600160401b031681526020808201929092526040908101600020805460ff19169315159390931790925590518481527f3b989d183b84b02259d7c14b34a9c9eb0fccb4c355a920d25e581e25aef4993d91016117d4565b60008381526010602052604081206001600160401b0380841690851610613449576040517f56607cb000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805160e081018252600183015481526002830154602082015260038301549181019190915260048201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c08201526134b890613631565b6134c2858561453e565b6001600160401b03166121169190613d8d565b60006105ba83836136d8565b600081815260018301602052604081205461352857508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611059565b506000611059565b60006105ba83836134e1565b600061105961354a836137d2565b6137df565b60006110597f00000000000000000000000000000000000000000000000000000000000000008361466b565b60008060ff831661358d600143613d7a565b6135979190613d7a565b40905060008190036135ab576135ab613e03565b60408051602081018390520160405160208183030381529060405280519060200120915050919050565b60608160000180548060200260200160405190810160405280929190818152602001828054801561362557602002820191906000526020600020905b815481526020019060010190808311613611575b50505050509050919050565b600081608001516001600160401b031682602001516110599190613d8d565b60006001600160401b03808416908316106105ba565b7fff00000000000000000000000000000000000000000000000000000000000000811660015b60208110156106de57600891821c916136a6908290613d8d565b83901b7fff0000000000000000000000000000000000000000000000000000000000000016919091179060010161368c565b600081815260018301602052604081205480156137c15760006136fc600183613d7a565b855490915060009061371090600190613d7a565b905081811461377557600086600001828154811061373057613730614528565b906000526020600020015490508087600001848154811061375357613753614528565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061378657613786614699565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611059565b6000915050611059565b5092915050565b6000611059826001613d5b565b60006110597f000000000000000000000000000000000000000000000000000000000000000083614571565b60408051610100810182526000608080830182815260a080850184905260c0850184905260e08501849052908452845190810185528281526020808201849052818601849052606080830185905292820192909252818401528351908101845290815290918201905b8152600060209091015290565b508054600082559060005260206000209081019061127991905b808211156138af576000815560010161389b565b5090565b6000602082840312156138c557600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b60208101600583106138f6576138f66138cc565b91905290565b6001600160a01b038116811461127957600080fd5b60008060006060848603121561392657600080fd5b833592506020840135613938816138fc565b91506040840135613948816138fc565b809150509250925092565b6001600160401b038116811461127957600080fd5b803561397381613953565b919050565b6000806040838503121561398b57600080fd5b82359150602083013561399d81613953565b809150509250929050565b600061010082840312156106de57600080fd5b600080600061014084860312156139d157600080fd5b8335925060208401356139e381613953565b91506139f285604086016139a8565b90509250925092565b602080825282518282018190526000918401906040840190835b81811015613a33578351835260209384019390920191600101613a15565b509095945050505050565b6000815180845260005b81811015613a6457602081850181015186830182015201613a48565b506000602082860101526020601f19601f83011685010191505092915050565b6001600160401b0381511682526001600160401b03602082015116602083015260ff604082015116604083015260ff60608201511660608301526000608082015160a060808501526110c960a0850182613a3e565b602081526000825160ff815116602084015260ff602082015116604084015260ff604082015116606084015260ff606082015116608084015250602083015160e060a0840152613b2d610100840182613a84565b90506040840151613b4460c08501825160ff169052565b5060608401516001600160401b03811660e0850152509392505050565b600060208284031215613b7357600080fd5b81356001600160401b03811115613b8957600080fd5b820161016081850312156105ba57600080fd5b6000815160408452613bb16040850182613a3e565b602093840151949093019390935250919050565b6001600160a01b038151168252600060208201518051602085015260208101516040850152604081015160608501526001600160401b0360608201511660808501526001600160401b0360808201511660a08501526001600160401b0360a08201511660c08501526001600160401b0360c08201511660e0850152506040820151610160610100850152613c5d610160850182613b9c565b90506060830151613c7a6101208601826001600160401b03169052565b5060808301516101408501528091505092915050565b602081526000825160406020840152613cac6060840182613bc5565b90506001600160401b0360208501511660408401528091505092915050565b60208101600783106138f6576138f66138cc565b60008060408385031215613cf257600080fd5b82359150602083013561399d816138fc565b6000806101208385031215613d1857600080fd5b82359150613d2984602085016139a8565b90509250929050565b6020815260006105ba6020830184613bc5565b634e487b7160e01b600052601160045260246000fd5b6001600160401b03818116838216019081111561105957611059613d45565b8181038181111561105957611059613d45565b808202811582820484141761105957611059613d45565b634e487b7160e01b600052601260045260246000fd5b600082613dc957613dc9613da4565b500490565b8082018082111561105957611059613d45565b600060208284031215613df357600080fd5b815180151581146105ba57600080fd5b634e487b7160e01b600052600160045260246000fd5b61ffff828116828216039081111561105957611059613d45565b600181811c90821680613e4757607f821691505b6020821081036106de57634e487b7160e01b600052602260045260246000fd5b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715613e9f57613e9f613e67565b60405290565b60405160a081016001600160401b0381118282101715613e9f57613e9f613e67565b60405160e081016001600160401b0381118282101715613e9f57613e9f613e67565b604051601f8201601f191681016001600160401b0381118282101715613f1157613f11613e67565b604052919050565b600060408284031215613f2b57600080fd5b613f33613e7d565b905081356001600160401b03811115613f4b57600080fd5b8201601f81018413613f5c57600080fd5b80356001600160401b03811115613f7557613f75613e67565b613f88601f8201601f1916602001613ee9565b818152856020838501011115613f9d57600080fd5b81602084016020830137600060209282018301528352928301359282019290925292915050565b6000813603610160811215613fd857600080fd5b613fe0613ea5565b8335613feb816138fc565b815260e0601f1983011215613fff57600080fd5b614007613ec7565b6020858101358252604080870135918301919091526060860135908201529150608084013561403581613953565b606083015260a084013561404881613953565b608083015260c084013561405b81613953565b60a083015260e084013561406e81613953565b60c08301526020810191909152610100830135906001600160401b0382111561409657600080fd5b6140a236838601613f19565b60408201526140b46101208501613968565b6060820152610140939093013560808401525090919050565b6000602082840312156140df57600080fd5b81356105ba816138fc565b6000602082840312156140fc57600080fd5b81356105ba81613953565b60008235603e1983360301811261411d57600080fd5b9190910192915050565b6000808335601e1984360301811261413e57600080fd5b8301803591506001600160401b0382111561415857600080fd5b6020019150368190038213156131ae57600080fd5b6000813561105981613953565b601f82111561094357806000526020600020601f840160051c810160208510156141a15750805b601f840160051c820191505b818110156141c157600081556001016141ad565b5050505050565b8135601e198336030181126141dc57600080fd5b820180356001600160401b03811180156141f557600080fd5b81360360208401131561420757600080fd5b6000905061421f826142198654613e33565b8661417a565b80601f8311600181146142545782841561423c5750848201602001355b600019600386901b1c1916600185901b1786556142b3565b600086815260209020601f19851690845b8281101561428757602085890181013583559485019460019092019101614265565b50858210156142a75760001960f88760031b161c19602085890101351681555b505060018460011b0186555b505050505060209190910135600190910155565b81356142d2816138fc565b815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b039190911617815560208201356001820155604082013560028201556060820135600382015560048101608083013561432a81613953565b815467ffffffffffffffff19166001600160401b0382161782555060a083013561435381613953565b81546fffffffffffffffff0000000000000000191660409190911b6fffffffffffffffff0000000000000000161781556143c761439260c0850161416d565b825467ffffffffffffffff60801b191660809190911b77ffffffffffffffff0000000000000000000000000000000016178255565b61441f6143d660e0850161416d565b825477ffffffffffffffffffffffffffffffffffffffffffffffff1660c09190911b7fffffffffffffffff00000000000000000000000000000000000000000000000016178255565b5061443a614431610100840184614107565b600583016141c8565b61446b61444a610120840161416d565b600783016001600160401b0382166001600160401b03198254161781555050565b6101409190910135600890910155565b83815282356020808301919091528301356040808301919091528301356060808301919091526101208201908401356144b381613953565b6001600160401b03811660808401525060808401356144d181613953565b6001600160401b03811660a08401525060a08401356144ef81613953565b6001600160401b03811660c08401525061450b60c08501613968565b6001600160401b0390811660e084015283166101008301526110c9565b634e487b7160e01b600052603260045260246000fd5b6001600160401b03828116828216039081111561105957611059613d45565b60008261456c5761456c613da4565b500690565b6001600160401b0381811683821602908116908181146137cb576137cb613d45565b60006001600160401b038316806145ac576145ac613da4565b806001600160401b0384160691505092915050565b8235815260208084013590820152600061012082016145f0604084016040870180358252602090810135910152565b61460a608084016080870180358252602090810135910152565b61462460c0840160c0870180358252602090810135910152565b610120610100840152835190819052602084019061014084019060005b8181101561465f578351835260209384019390920191600101614641565b50909695505050505050565b60006001600160401b0383168061468457614684613da4565b806001600160401b0384160491505092915050565b634e487b7160e01b600052603160045260246000fdfea26469706673582212206e940e8cb5c54ce6f6606e3b8f89e55d51c501d4017adbc3ac7b30357327730d64736f6c634300081c0033"; + public static string BYTECODE = "0x60c060405234801561001057600080fd5b50604051614f41380380614f4183398101604081905261002f9161053b565b602083015180516040850151516001805460ff191660ff90921691909117905582906001600160401b03811660000361007b5760405163015536c760e51b815260040160405180910390fd5b6001600160401b031660805261010043116100a9576040516338f5f66160e11b815260040160405180910390fd5b8151600280546020850151604086015160608701516001600160401b039586166001600160801b0319909416939093176801000000000000000095909216949094021761ffff60801b1916600160801b60ff9485160260ff60881b191617600160881b9390911692909202919091178155608083015183919060039061012f90826106d9565b5050600480546001600160a01b0319166001600160a01b0393841617905550831660a05250825151606460ff909116111561017d576040516302bd816360e41b815260040160405180910390fd5b606483600001516040015160ff1611156101aa576040516354e5e0ab60e11b815260040160405180910390fd5b825160408101516020909101516064916101c391610797565b60ff1611156101e5576040516317ff9d0f60e21b815260040160405180910390fd5b82518051600b805460208085015160408087015160609788015160ff90811663010000000263ff0000001992821662010000029290921663ffff0000199482166101000261ffff1990971698821698909817959095179290921695909517178355808801518051600c80549383015196830151978301518516600160881b0260ff60881b1998909516600160801b029790971661ffff60801b196001600160401b0397881668010000000000000000026001600160801b031990951697909216969096179290921791909116939093171783556080820151869391929190600d906102d090826106d9565b50505060408201515160038201805460ff191660ff909216919091179055606090910151600490910180546001600160401b0319166001600160401b03909216919091179055506107c8915050565b634e487b7160e01b600052604160045260246000fd5b60405160a081016001600160401b03811182821017156103575761035761031f565b60405290565b604051608081016001600160401b03811182821017156103575761035761031f565b604051601f8201601f191681016001600160401b03811182821017156103a7576103a761031f565b604052919050565b805160ff811681146103c057600080fd5b919050565b80516001600160401b03811681146103c057600080fd5b600060a082840312156103ee57600080fd5b6103f6610335565b9050610401826103c5565b815261040f602083016103c5565b6020820152610420604083016103af565b6040820152610431606083016103af565b606082015260808201516001600160401b0381111561044f57600080fd5b8201601f8101841361046057600080fd5b80516001600160401b038111156104795761047961031f565b61048c601f8201601f191660200161037f565b8181528560208385010111156104a157600080fd5b60005b828110156104c0576020818501810151838301820152016104a4565b5060006020838301015280608085015250505092915050565b6000602082840312156104eb57600080fd5b604051602081016001600160401b038111828210171561050d5761050d61031f565b60405290508061051c836103af565b905292915050565b80516001600160a01b03811681146103c057600080fd5b60008060006060848603121561055057600080fd5b83516001600160401b0381111561056657600080fd5b840180860360e081121561057957600080fd5b61058161035d565b608082121561058f57600080fd5b61059761035d565b91506105a2836103af565b82526105b0602084016103af565b60208301526105c1604084016103af565b60408301526105d2606084016103af565b60608301529081526080820151906001600160401b038211156105f457600080fd5b610600888385016103dc565b60208201526106128860a085016104d9565b604082015261062360c084016103c5565b6060820152945061063991505060208501610524565b915061064760408501610524565b90509250925092565b600181811c9082168061066457607f821691505b60208210810361068457634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156106d457806000526020600020601f840160051c810160208510156106b15750805b601f840160051c820191505b818110156106d157600081556001016106bd565b50505b505050565b81516001600160401b038111156106f2576106f261031f565b610706816107008454610650565b8461068a565b6020601f82116001811461073a57600083156107225750848201515b600019600385901b1c1916600184901b1784556106d1565b600084815260208120601f198516915b8281101561076a578785015182556020948501946001909201910161074a565b50848210156107885786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b60ff81811683821602908116908181146107c157634e487b7160e01b600052601160045260246000fd5b5092915050565b60805160a05161471c610825600039600081816104bf01528181610f700152818161201901528181612603015281816126b301528181612842015281816128f20152612d1401526000818161355d015261384f015261471c6000f3fe608060405234801561001057600080fd5b50600436106101c45760003560e01c80636e2b54ee116100f9578063c0cc4add11610097578063e8aa0a0711610071578063e8aa0a0714610461578063f752196b14610474578063fb1e61ca1461049d578063fc0c546a146104bd57600080fd5b8063c0cc4add14610428578063c5d433511461043b578063d02bbe331461044e57600080fd5b8063a29c29a4116100d3578063a29c29a4146103b2578063a3a0807e146103c5578063b396dc79146103e8578063be5cdc481461040857600080fd5b80636e2b54ee146103845780639777b72c1461039757806399b6da0c1461039f57600080fd5b8063329b5a0b1161016657806351a766421161014057806351a76642146103035780635da73835146103165780636b00c8cf1461032b5780636c70bee91461036f57600080fd5b8063329b5a0b14610298578063458d2bf1146102cb5780634641dce6146102de57600080fd5b806312827602116101a2578063128276021461022e5780631d873c1b14610241578063237d84821461025457806326d6f8341461026757600080fd5b806302fa8e65146101c957806305b90773146101f95780630aefaabe14610219575b600080fd5b6101dc6101d73660046138ea565b6104e3565b6040516001600160401b0390911681526020015b60405180910390f35b61020c6102073660046138ea565b6105c1565b6040516101f09190613919565b61022c610227366004613948565b6106e4565b005b61022c61023c3660046139af565b610877565b61022c61024f3660046139f2565b610948565b61022c6102623660046139af565b610dfa565b61028a6102753660046138ea565b60009081526012602052604090206003015490565b6040519081526020016101f0565b6101dc6102a63660046138ea565b600090815260116020526040902060020154600160c01b90046001600160401b031690565b61028a6102d93660046138ea565b611046565b6102f16102ec3660046138ea565b61105f565b60405160ff90911681526020016101f0565b61028a6103113660046138ea565b611072565b61031e6110d1565b6040516101f09190613a32565b6103576103393660046138ea565b6000908152601260205260409020600401546001600160a01b031690565b6040516001600160a01b0390911681526020016101f0565b6103776110f8565b6040516101f09190613b10565b61022c6103923660046138ea565b61126f565b61031e61127c565b61022c6103ad366004613b98565b61129b565b61022c6103c03660046138ea565b6117e1565b6103d86103d33660046138ea565b611833565b60405190151581526020016101f0565b6103fb6103f63660046138ea565b61186f565b6040516101f09190613cc7565b61041b6104163660046138ea565b611b51565b6040516101f09190613d02565b6103d86104363660046138ea565b611c1f565b61022c610449366004613d16565b611c32565b6103d861045c3660046139af565b6120b3565b61022c61046f366004613d3b565b612155565b6101dc6104823660046138ea565b6000908152600660205260409020546001600160401b031690565b6104b06104ab3660046138ea565b6122ce565b6040516101f09190613d69565b7f0000000000000000000000000000000000000000000000000000000000000000610357565b6000806104ef836105c1565b9050600081600481111561050557610505613903565b14806105225750600181600481111561052057610520613903565b145b1561054e575050600090815260116020526040902060020154600160801b90046001600160401b031690565b600281600481111561056257610562613903565b0361058e575050600090815260116020526040902060020154600160c01b90046001600160401b031690565b6000838152601160205260409020600201546105ba90600160801b90046001600160401b0316426124e4565b9392505050565b60008181526010602052604081205482906001600160a01b03166105f857604051635eeb253d60e11b815260040160405180910390fd5b600083815260116020526040812090815460ff16600481111561061d5761061d613903565b14801561065c5750600084815260116020526040902060020154600160c01b90046001600160401b03166001600160401b0316426001600160401b0316115b1561066b5760029250506106de565b6001815460ff16600481111561068357610683613903565b14806106a457506000815460ff1660048111156106a2576106a2613903565b145b80156106c8575060028101546001600160401b03600160801b909104811642909116115b156106d75760039250506106de565b5460ff1691505b50919050565b826000808281526012602052604090205460ff16600681111561070957610709613903565b0361072757604051638b41ec7f60e01b815260040160405180910390fd5b600084815260126020526040902060048101546001600160a01b0316331461077b576040517f57a6f4e900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600061078686611b51565b9050600481600681111561079c5761079c613903565b036107d3576040517fc2cbf77700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60028160068111156107e7576107e7613903565b03610801576107fc82600101548787876124f4565b61086f565b600581600681111561081557610815613903565b0361082a576107fc826001015487878761273d565b600381600681111561083e5761083e613903565b0361084d576107fc3387612986565b600181600681111561086157610861613903565b0361086f5761086f866129a8565b505050505050565b61088182826120b3565b6108b7576040517f424a04ab00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006108c38383612bf8565b60008181526020819052604090209091506108de9033612c3d565b50600154600082815260208190526040902060ff909116906108ff90612c52565b03610943576040516001600160401b038316815283907fc8e6c955744189a19222ec226b72ac1435d88d5745252dac56e6f679f64c037a9060200160405180910390a25b505050565b60008381526010602052604090205483906001600160a01b031661097f57604051635eeb253d60e11b815260040160405180910390fd5b600084815260106020526040902060048101546001600160401b03908116908516106109d7576040517f3b920b8800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60006109e38686612bf8565b60008181526020819052604090209091506109fe9033612c5c565b610a34576040517fd651ce1800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000818152601260209081526040808320600181018a90556002810180546fffffffffffffffff00000000000000001916600160401b6001600160401b038c1602179055898452601190925282209091610a8d84611b51565b6006811115610a9e57610a9e613903565b14158015610ac657506006610ab284611b51565b6006811115610ac357610ac3613903565b14155b15610afd576040517fff556acf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60048201805473ffffffffffffffffffffffffffffffffffffffff1916331790556002820180546001600160401b03421667ffffffffffffffff19909116179055610b6e83600090815260056020526040902080546001600160401b03421667ffffffffffffffff19909116179055565b610b788387612155565b60028101805460019190600090610b999084906001600160401b0316613d92565b92506101000a8154816001600160401b0302191690836001600160401b03160217905550610bde888360020160009054906101000a90046001600160401b0316612c7e565b816001016000828254610bf19190613db1565b90915550506040805160e081018252600186015481526002860154602082015260038601549181019190915260048501546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c08201526000908190610c6a90612cad565b90506006610c7786611b51565b6006811115610c8857610c88613903565b03610cbb57600b54606490610ca09060ff1683613dc4565b610caa9190613df1565b610cb49082613db1565b9150610cbf565b8091505b610cc93383612ccc565b8160136000016000828254610cde9190613e05565b9091555050600384018190556004840154610d02906001600160a01b031686612da0565b835460ff191660011784556040516001600160401b038a1681528a907f8f301470a994578b52323d625dfbf827ca5208c81747d3459be7b8867baec3ec9060200160405180910390a2600486015460028401546001600160401b039081169116148015610d8457506000835460ff166004811115610d8257610d82613903565b145b15610dee57825460ff191660011783556002830180546001600160401b034216600160401b026fffffffffffffffff0000000000000000199091161790556040518a907f85e1543bf2f84fe80c6badbce3648c8539ad1df4d2b3d822938ca0538be727e690600090a25b50505050505050505050565b6001610e0583611b51565b6006811115610e1657610e16613903565b14610e4d576040517fae9dcffd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610e578282612dc2565b6000828152601260209081526040808320600180820154855260108452828520600b54845160e08101865292820154835260028201549583019590955260038101549382019390935260048301546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c0820152909391926064916201000090910460ff1690610efa90612cad565b610f049190613dc4565b610f0e9190613df1565b600b54909150600090606490610f2e906301000000900460ff1684613dc4565b610f389190613df1565b90508060136001016000828254610f4f9190613e05565b909155505060405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303816000875af1158015610fc1573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610fe59190613e18565b610ff157610ff1613e3a565b818460030160008282546110059190613db1565b9091555050600b5460008781526006602052604090205461010090910460ff16906001600160401b03166001600160401b03161061086f5761086f866129a8565b600061105982611054612fd9565b612fe4565b92915050565b60006110598261106d612fd9565b612ff8565b60008181526012602090815260408083206001810154845260109092528220600c54610100906110ac90600160801b900460ff1682613e50565b60018301546110bf9161ffff1690613dc4565b6110c99190613df1565b949350505050565b336000908152600a602052604090206060906110f3906110f09061308a565b90565b905090565b611100613874565b604080516101008082018352600b805460ff8082166080808701918252948304821660a080880191909152620100008404831660c08801526301000000909304821660e0870152855285519182018652600c80546001600160401b038082168552600160401b820416602085810191909152600160801b82048416988501989098527101000000000000000000000000000000000090049091166060830152600d80549596939593870194929391928401916111bb90613e6a565b80601f01602080910402602001604051908101604052809291908181526020018280546111e790613e6a565b80156112345780601f1061120957610100808354040283529160200191611234565b820191906000526020600020905b81548152906001019060200180831161121757829003601f168201915b5050509190925250505081526040805160208181018352600385015460ff1682528301526004909201546001600160401b0316910152919050565b6112798133611c32565b50565b3360009081526009602052604090206060906110f3906110f09061308a565b60006112ae6112a983613ffb565b613097565b9050336112be6020840184614104565b6001600160a01b0316146112e5576040516334c69e3160e11b815260040160405180910390fd5b6000818152601060205260409020546001600160a01b031615611334576040517ffc7d069000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61134661014083016101208401614121565b6001600160401b0316158061138d575061136660e0830160c08401614121565b6001600160401b031661138161014084016101208501614121565b6001600160401b031610155b156113c4576040517fdf63f61a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6113d460a0830160808401614121565b6001600160401b0316600003611416576040517f535ed2be00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61142660a0830160808401614121565b6001600160401b0316611440610100840160e08501614121565b6001600160401b03161115611481576040517fb9551ab100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61149160e0830160c08401614121565b6001600160401b03166000036114d3576040517f090a5ecd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6020820135600003611511576040517f6aba7aae00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b606082013560000361154f576040517ffb7df0c700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604082013560000361158d576040517f47ba51c700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b61159b61010083018361413e565b6115a5908061415e565b90506000036115e0576040517f86f8cf9b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600f546001600160401b03166115fc60e0840160c08501614121565b6001600160401b0316111561163d576040517f1267b3f200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000818152601060205260409020829061165782826142fe565b5061166a905060e0830160c08401614121565b6116749042613d92565b600082815260116020526040902060020180546001600160401b0392909216600160801b0267ffffffffffffffff60801b199092169190911790556116c161014083016101208401614121565b6116cb9042613d92565b600082815260116020908152604090912060020180546001600160401b0393909316600160c01b0277ffffffffffffffffffffffffffffffffffffffffffffffff9093169290921790915561172c9061172690840184614104565b826130c7565b600061173f61173a84613ffb565b6130e9565b600083815260116020526040812060010182905560138054929350839290919061176a908490613e05565b9091555061177a90503382612ccc565b6000828152601160209081526040918290206002015491517f1bf9c457accf8703dbf7cdf1b58c2f74ddf2e525f98155c70b3d318d74609bd8926117d492869290880191600160c01b90046001600160401b0316906144b2565b60405180910390a1505050565b806000808281526012602052604090205460ff16600681111561180657611806613903565b0361182457604051638b41ec7f60e01b815260040160405180910390fd5b61182f8233336106e4565b5050565b600080600061184984611844612fd9565b613125565b90925090508180156110c95750600254600160801b900460ff9081169116109392505050565b6118f260405180604001604052806138dd6040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b816000808281526012602052604090205460ff16600681111561191757611917613903565b0361193557604051638b41ec7f60e01b815260040160405180910390fd5b60008381526012602052604090206119c660405180604001604052806138dd6040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b600180830154600090815260106020908152604091829020825160a0808201855282546001600160a01b03168252845160e08101865295830154865260028301548685015260038301548686015260048301546001600160401b038082166060890152600160401b820481166080890152600160801b8204811692880192909252600160c01b90041660c0860152918201939093528151808301835260058401805492949385019282908290611a7b90613e6a565b80601f0160208091040260200160405190810160405280929190818152602001828054611aa790613e6a565b8015611af45780601f10611ac957610100808354040283529160200191611af4565b820191906000526020600020905b815481529060010190602001808311611ad757829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0390811683830152600890930154604090920191909152918352600290930154600160401b900490921691810191909152915050919050565b600081815260126020526040812060018101548203611b735750600092915050565b6000611b8282600101546105c1565b90506004825460ff166006811115611b9c57611b9c613903565b03611bab575060049392505050565b6002816004811115611bbf57611bbf613903565b03611bce575060059392505050565b6003816004811115611be257611be2613903565b03611bf1575060029392505050565b6004816004811115611c0557611c05613903565b03611c14575060039392505050565b505460ff1692915050565b600061105982611c2d612fd9565b6131dd565b60008281526010602052604090205482906001600160a01b0316611c6957604051635eeb253d60e11b815260040160405180910390fd5b6000838152601060209081526040808320601190925290912081546001600160a01b03163314611cac576040516334c69e3160e11b815260040160405180910390fd5b6000611cb7866105c1565b90506002816004811115611ccd57611ccd613903565b14158015611ced57506004816004811115611cea57611cea613903565b14155b8015611d0b57506003816004811115611d0857611d08613903565b14155b15611d42576040517fc00b5b5700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8160010154600003611d80576040517fbd8bdd9400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6002816004811115611d9457611d94613903565b03611e3257815460ff1916600217825560405186907ff903f4774c7bd27355f9d7fcbc382b079b164a697a44ac5d95267a4c3cb3bb2290600090a2600086815260116020526040902060020154611dfc908790600160c01b90046001600160401b0316612c7e565b6002830154611e1491906001600160401b0316613dc4565b826001016000828254611e279190613e05565b90915550611fbf9050565b6004816004811115611e4657611e46613903565b03611fb3576040805160a0808201835285546001600160a01b03168252825160e08101845260018701548152600287015460208281019190915260038801548286015260048801546001600160401b038082166060850152600160401b820481166080850152600160801b8204811694840194909452600160c01b900490921660c08201529082015281518083018352600586018054611fa994889390850192909182908290611ef590613e6a565b80601f0160208091040260200160405190810160405280929190818152602001828054611f2190613e6a565b8015611f6e5780601f10611f4357610100808354040283529160200191611f6e565b820191906000526020600020905b815481529060010190602001808311611f5157829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0316908201526008909101546040909101526130e9565b6001830155611fbf565b815460ff191660031782555b8254611fd4906001600160a01b031687613217565b600182015460148054829190600090611fee908490613e05565b909155505060405163a9059cbb60e01b81526001600160a01b038781166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af1158015612062573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120869190613e18565b6120a357604051637c2ccffd60e11b815260040160405180910390fd5b5050600060019091015550505050565b600033816120c18585612bf8565b905060006120ce82611b51565b905060008160068111156120e4576120e4613903565b1480612101575060068160068111156120ff576120ff613903565b145b801561212a5750600154600083815260208190526040902060ff9091169061212890612c52565b105b801561214b575060008281526020819052604090206121499084612c5c565b155b9695505050505050565b6000828152601260209081526040808320600101548084526010909252909120546001600160a01b031661219c57604051635eeb253d60e11b815260040160405180910390fd5b600083815260126020526040902060048101546001600160a01b031633146121f0576040517fce351b9400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001810154600090815260106020526040808220815160038082526080820190935290929181602001602082028036833701905050905061223861223387611046565b613239565b8160008151811061224b5761224b61455f565b602090810291909101015260068201546122649061324a565b816001815181106122775761227761455f565b6020026020010181815250508260020160089054906101000a90046001600160401b03166001600160401b0316816002815181106122b7576122b761455f565b60200260200101818152505061086f868683613256565b6123436040805160a080820183526000808352835160e081018552818152602080820183905281860183905260608083018490526080830184905293820183905260c0820183905280850191909152845180860186529283528201529091820190815260006020820181905260409091015290565b60008281526010602052604090205482906001600160a01b031661237a57604051635eeb253d60e11b815260040160405180910390fd5b600083815260106020908152604091829020825160a0808201855282546001600160a01b03168252845160e0810186526001840154815260028401548186015260038401548187015260048401546001600160401b038082166060840152600160401b820481166080840152600160801b8204811693830193909352600160c01b900490911660c0820152928101929092528251808401845260058201805493949293928501928290829061242e90613e6a565b80601f016020809104026020016040519081016040528092919081815260200182805461245a90613e6a565b80156124a75780601f1061247c576101008083540402835291602001916124a7565b820191906000526020600020905b81548152906001019060200180831161248a57829003601f168201915b50505091835250506001919091015460209182015290825260078301546001600160401b0316908201526008909101546040909101529392505050565b60008282188284100282186105ba565b60008481526010602052604090205484906001600160a01b031661252b57604051635eeb253d60e11b815260040160405180910390fd5b600085815260116020908152604080832060108352818420815460ff1916600317825588855260129093529220815461256d906001600160a01b031689613217565b6004810154612585906001600160a01b031688612986565b60028101546000906125a1908a906001600160401b0316612c7e565b60038301549091506125b38183613e05565b601480546000906125c5908490613e05565b90915550508254600490849060ff1916600183021790555060405163a9059cbb60e01b81526001600160a01b038981166004830152602482018490527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561264c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906126709190613e18565b61268d57604051637c2ccffd60e11b815260040160405180910390fd5b60405163a9059cbb60e01b81526001600160a01b038881166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af11580156126fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906127209190613e18565b610dee57604051637c2ccffd60e11b815260040160405180910390fd5b60008481526010602052604090205484906001600160a01b031661277457604051635eeb253d60e11b815260040160405180910390fd5b6000848152601260205260409020600481015461279a906001600160a01b031686612986565b60028101546000906127e09088906001600160401b03166127db826000908152601160205260409020600201546001600160401b03600160c01b9091041690565b6133f4565b60038301549091506127f28183613e05565b60148054600090612804908490613e05565b90915550508254600490849060ff1916600183021790555060405163a9059cbb60e01b81526001600160a01b038781166004830152602482018490527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561288b573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906128af9190613e18565b6128cc57604051637c2ccffd60e11b815260040160405180910390fd5b60405163a9059cbb60e01b81526001600160a01b038681166004830152602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af115801561293b573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061295f9190613e18565b61297c57604051637c2ccffd60e11b815260040160405180910390fd5b5050505050505050565b6001600160a01b0382166000908152600a6020526040902061094390826134d3565b600081815260126020908152604080832060018101548085526011909352922060028301546129e19083906001600160401b0316612c7e565b8160010160008282546129f49190613e05565b90915550506004830154612a11906001600160a01b031685612986565b6000848152602081905260409020612a28906134df565b825460ff191660061783556002808401805467ffffffffffffffff1916905560006003850181905560048501805473ffffffffffffffffffffffffffffffffffffffff19169055908201805460019290612a8c9084906001600160401b0316614575565b82546101009290920a6001600160401b038181021990931691831602179091556002850154604051600160401b90910490911681528391507f33ba8f7627565d89f7ada2a6b81ea532b7aa9b11e91a78312d6e1fca0bfcd1dc9060200160405180910390a26000848152600660205260409020805467ffffffffffffffff19169055600082815260106020526040812060028301546004820154919291612b3f916001600160401b039081169116614575565b60048301546001600160401b039182169250600160c01b90041681118015612b7c57506001835460ff166004811115612b7a57612b7a613903565b145b1561086f57825460ff19166004178355612b97600142614575565b6002840180546001600160401b0392909216600160801b0267ffffffffffffffff60801b1990921691909117905560405184907f4769361a442504ecaf038f35e119bcccdd5e42096b24c09e3c17fd17c6684c0290600090a2505050505050565b60008282604051602001612c1f9291909182526001600160401b0316602082015260400190565b60405160208183030381529060405280519060200120905092915050565b60006105ba836001600160a01b0384166134e8565b6000611059825490565b6001600160a01b038116600090815260018301602052604081205415156105ba565b6000828152601160205260408120600201546105ba9084908490600160801b90046001600160401b03166133f4565b600081608001516001600160401b031682604001516110599190613dc4565b6040517f23b872dd0000000000000000000000000000000000000000000000000000000081526001600160a01b038381166004830152306024830181905260448301849052917f0000000000000000000000000000000000000000000000000000000000000000909116906323b872dd906064016020604051808303816000875af1158015612d5f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612d839190613e18565b61094357604051637c2ccffd60e11b815260040160405180910390fd5b6001600160a01b0382166000908152600a602052604090206109439082613537565b6000612dcd82613543565b6001600160401b03169050428110612e11576040517f6b4b1a4e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600254612e2e90600160401b90046001600160401b031682613e05565b4210612e66576040517fde55698e00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526007602090815260408083206001600160401b038616845290915290205460ff1615612ec2576040517efab7d900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b612ecc83836131dd565b612f02576040517fd3ffa66b00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526008602090815260408083206001600160401b038616845290915290205460ff1615612f5f576040517f98e7e55100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008381526008602090815260408083206001600160401b038087168552908352818420805460ff1916600190811790915587855260069093529083208054929390929091612fb091859116613d92565b92506101000a8154816001600160401b0302191690836001600160401b03160217905550505050565b60006110f342613556565b60006105ba612ff38484612ff8565b613582565b60008061300761010043614594565b600254909150600090610100906130369071010000000000000000000000000000000000900460ff16866145a8565b61304091906145ca565b6001600160401b03169050600061305961010087614594565b905060006101008261306b8587613e05565b6130759190613e05565b61307f9190614594565b979650505050505050565b606060006105ba836135dc565b6000816040516020016130aa9190613d69565b604051602081830303815290604052805190602001209050919050565b6001600160a01b03821660009081526009602052604090206109439082613537565b60006130f88260200151613638565b602083015160a081015160609091015161311291906145a8565b6001600160401b03166110599190613dc4565b600080600061313385611b51565b60008681526005602052604081205491925090613158906001600160401b0316613556565b9050600182600681111561316e5761316e613903565b14158061318257506131808582613657565b155b15613195576000809350935050506131d6565b61319f8686612ff8565b925060006131ac84613582565b905060006131b988611072565b90508015806131cf57506131cd8183614594565b155b9550505050505b9250929050565b60008060006131ec8585613125565b909250905081801561320e575060025460ff600160801b909104811690821610155b95945050505050565b6001600160a01b038216600090815260096020526040902061094390826134d3565b600060ff198216816110c98261366d565b6000806105ba8361366d565b60008381526007602052604081209061326d612fd9565b6001600160401b0316815260208101919091526040016000205460ff16156132c1576040517f3edef7db00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600480546040517f94c8919d0000000000000000000000000000000000000000000000000000000081526001600160a01b03909116916394c8919d9161330b9186918691016145f8565b602060405180830381865afa158015613328573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061334c9190613e18565b613382576040517ffcd03a4700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600083815260076020526040812060019161339b612fd9565b6001600160401b031681526020808201929092526040908101600020805460ff19169315159390931790925590518481527f3b989d183b84b02259d7c14b34a9c9eb0fccb4c355a920d25e581e25aef4993d91016117d4565b60008381526010602052604081206001600160401b0380841690851610613447576040517f56607cb000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6040805160e081018252600183015481526002830154602082015260038301549181019190915260048201546001600160401b038082166060840152600160401b820481166080840152600160801b8204811660a0840152600160c01b9091041660c08201526134b690613638565b6134c08585614575565b6001600160401b031661320e9190613dc4565b60006105ba83836136df565b611279816137d9565b600081815260018301602052604081205461352f57508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155611059565b506000611059565b60006105ba83836134e8565b60006110596135518361383b565b613848565b60006110597f0000000000000000000000000000000000000000000000000000000000000000836146a2565b60008060ff8316613594600143613db1565b61359e9190613db1565b40905060008190036135b2576135b2613e3a565b60408051602081018390520160405160208183030381529060405280519060200120915050919050565b60608160000180548060200260200160405190810160405280929190818152602001828054801561362c57602002820191906000526020600020905b815481526020019060010190808311613618575b50505050509050919050565b600081608001516001600160401b031682602001516110599190613dc4565b60006001600160401b03808416908316106105ba565b7fff00000000000000000000000000000000000000000000000000000000000000811660015b60208110156106de57600891821c916136ad908290613dc4565b83901b7fff00000000000000000000000000000000000000000000000000000000000000169190911790600101613693565b600081815260018301602052604081205480156137c8576000613703600183613db1565b855490915060009061371790600190613db1565b905080821461377c5760008660000182815481106137375761373761455f565b906000526020600020015490508087600001848154811061375a5761375a61455f565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061378d5761378d6146d0565b600190038181906000526020600020016000905590558560010160008681526020019081526020016000206000905560019350505050611059565b6000915050611059565b5092915050565b60006137e3825490565b905060005b818110156138335782600101600084600001838154811061380b5761380b61455f565b90600052602060002001548152602001908152602001600020600090558060010190506137e8565b505060009055565b6000611059826001613d92565b60006110597f0000000000000000000000000000000000000000000000000000000000000000836145a8565b60408051610100810182526000608080830182815260a080850184905260c0850184905260e08501849052908452845190810185528281526020808201849052818601849052606080830185905292820192909252818401528351908101845290815290918201905b8152600060209091015290565b6000602082840312156138fc57600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b602081016005831061392d5761392d613903565b91905290565b6001600160a01b038116811461127957600080fd5b60008060006060848603121561395d57600080fd5b83359250602084013561396f81613933565b9150604084013561397f81613933565b809150509250925092565b6001600160401b038116811461127957600080fd5b80356139aa8161398a565b919050565b600080604083850312156139c257600080fd5b8235915060208301356139d48161398a565b809150509250929050565b600061010082840312156106de57600080fd5b60008060006101408486031215613a0857600080fd5b833592506020840135613a1a8161398a565b9150613a2985604086016139df565b90509250925092565b602080825282518282018190526000918401906040840190835b81811015613a6a578351835260209384019390920191600101613a4c565b509095945050505050565b6000815180845260005b81811015613a9b57602081850181015186830182015201613a7f565b506000602082860101526020601f19601f83011685010191505092915050565b6001600160401b0381511682526001600160401b03602082015116602083015260ff604082015116604083015260ff60608201511660608301526000608082015160a060808501526110c960a0850182613a75565b602081526000825160ff815116602084015260ff602082015116604084015260ff604082015116606084015260ff606082015116608084015250602083015160e060a0840152613b64610100840182613abb565b90506040840151613b7b60c08501825160ff169052565b5060608401516001600160401b03811660e0850152509392505050565b600060208284031215613baa57600080fd5b81356001600160401b03811115613bc057600080fd5b820161016081850312156105ba57600080fd5b6000815160408452613be86040850182613a75565b602093840151949093019390935250919050565b6001600160a01b038151168252600060208201518051602085015260208101516040850152604081015160608501526001600160401b0360608201511660808501526001600160401b0360808201511660a08501526001600160401b0360a08201511660c08501526001600160401b0360c08201511660e0850152506040820151610160610100850152613c94610160850182613bd3565b90506060830151613cb16101208601826001600160401b03169052565b5060808301516101408501528091505092915050565b602081526000825160406020840152613ce36060840182613bfc565b90506001600160401b0360208501511660408401528091505092915050565b602081016007831061392d5761392d613903565b60008060408385031215613d2957600080fd5b8235915060208301356139d481613933565b6000806101208385031215613d4f57600080fd5b82359150613d6084602085016139df565b90509250929050565b6020815260006105ba6020830184613bfc565b634e487b7160e01b600052601160045260246000fd5b6001600160401b03818116838216019081111561105957611059613d7c565b8181038181111561105957611059613d7c565b808202811582820484141761105957611059613d7c565b634e487b7160e01b600052601260045260246000fd5b600082613e0057613e00613ddb565b500490565b8082018082111561105957611059613d7c565b600060208284031215613e2a57600080fd5b815180151581146105ba57600080fd5b634e487b7160e01b600052600160045260246000fd5b61ffff828116828216039081111561105957611059613d7c565b600181811c90821680613e7e57607f821691505b6020821081036106de57634e487b7160e01b600052602260045260246000fd5b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715613ed657613ed6613e9e565b60405290565b60405160a081016001600160401b0381118282101715613ed657613ed6613e9e565b60405160e081016001600160401b0381118282101715613ed657613ed6613e9e565b604051601f8201601f191681016001600160401b0381118282101715613f4857613f48613e9e565b604052919050565b600060408284031215613f6257600080fd5b613f6a613eb4565b905081356001600160401b03811115613f8257600080fd5b8201601f81018413613f9357600080fd5b80356001600160401b03811115613fac57613fac613e9e565b613fbf601f8201601f1916602001613f20565b818152856020838501011115613fd457600080fd5b81602084016020830137600060209282018301528352928301359282019290925292915050565b600081360361016081121561400f57600080fd5b614017613edc565b833561402281613933565b815260e0601f198301121561403657600080fd5b61403e613efe565b6020858101358252604080870135918301919091526060860135908201529150608084013561406c8161398a565b606083015260a084013561407f8161398a565b608083015260c08401356140928161398a565b60a083015260e08401356140a58161398a565b60c08301526020810191909152610100830135906001600160401b038211156140cd57600080fd5b6140d936838601613f50565b60408201526140eb610120850161399f565b6060820152610140939093013560808401525090919050565b60006020828403121561411657600080fd5b81356105ba81613933565b60006020828403121561413357600080fd5b81356105ba8161398a565b60008235603e1983360301811261415457600080fd5b9190910192915050565b6000808335601e1984360301811261417557600080fd5b8301803591506001600160401b0382111561418f57600080fd5b6020019150368190038213156131d657600080fd5b600081356110598161398a565b601f82111561094357806000526020600020601f840160051c810160208510156141d85750805b601f840160051c820191505b818110156141f857600081556001016141e4565b5050505050565b8135601e1983360301811261421357600080fd5b820180356001600160401b038111801561422c57600080fd5b81360360208401131561423e57600080fd5b60009050614256826142508654613e6a565b866141b1565b80601f83116001811461428b578284156142735750848201602001355b600019600386901b1c1916600185901b1786556142ea565b600086815260209020601f19851690845b828110156142be5760208589018101358355948501946001909201910161429c565b50858210156142de5760001960f88760031b161c19602085890101351681555b505060018460011b0186555b505050505060209190910135600190910155565b813561430981613933565b815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b03919091161781556020820135600182015560408201356002820155606082013560038201556004810160808301356143618161398a565b815467ffffffffffffffff19166001600160401b0382161782555060a083013561438a8161398a565b81546fffffffffffffffff0000000000000000191660409190911b6fffffffffffffffff0000000000000000161781556143fe6143c960c085016141a4565b825467ffffffffffffffff60801b191660809190911b77ffffffffffffffff0000000000000000000000000000000016178255565b61445661440d60e085016141a4565b825477ffffffffffffffffffffffffffffffffffffffffffffffff1660c09190911b7fffffffffffffffff00000000000000000000000000000000000000000000000016178255565b5061447161446861010084018461413e565b600583016141ff565b6144a261448161012084016141a4565b600783016001600160401b0382166001600160401b03198254161781555050565b6101409190910135600890910155565b83815282356020808301919091528301356040808301919091528301356060808301919091526101208201908401356144ea8161398a565b6001600160401b03811660808401525060808401356145088161398a565b6001600160401b03811660a08401525060a08401356145268161398a565b6001600160401b03811660c08401525061454260c0850161399f565b6001600160401b0390811660e084015283166101008301526110c9565b634e487b7160e01b600052603260045260246000fd5b6001600160401b03828116828216039081111561105957611059613d7c565b6000826145a3576145a3613ddb565b500690565b6001600160401b0381811683821602908116908181146137d2576137d2613d7c565b60006001600160401b038316806145e3576145e3613ddb565b806001600160401b0384160691505092915050565b823581526020808401359082015260006101208201614627604084016040870180358252602090810135910152565b614641608084016080870180358252602090810135910152565b61465b60c0840160c0870180358252602090810135910152565b610120610100840152835190819052602084019061014084019060005b81811015614696578351835260209384019390920191600101614678565b50909695505050505050565b60006001600160401b038316806146bb576146bb613ddb565b806001600160401b0384160491505092915050565b634e487b7160e01b600052603160045260246000fdfea26469706673582212203444a73360bb50dd0606abd72470bb042d6e86a23b0e86c86dd00cdc8a0275f564736f6c634300081c0033"; public MarketplaceDeploymentBase() : base(BYTECODE) { } public MarketplaceDeploymentBase(string byteCode) : base(byteCode) { } [Parameter("tuple", "config", 1)] diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index 787727e5..01ffd4d7 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,7 +2,7 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.2-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-28a83db-dist-tests"; public static string Override { get; set; } = string.Empty; From a8c094e735f8799263a7e2d31d26ee8e5d1e1bf9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 30 May 2025 08:59:43 +0200 Subject: [PATCH 63/69] Cleanup and rename --- Tests/CodexReleaseTests/DataTests/DecodeTest.cs | 2 +- .../MarketTests/{ContractFailedTest.cs => FailTest.cs} | 6 +++--- .../{ContractSuccessfulTest.cs => FinishTest.cs} | 8 +++++--- Tests/CodexReleaseTests/MarketTests/RepairTest.cs | 4 +++- ...MultipleContractsTest.cs => SequentialContracts.cs} | 9 +++++---- .../{ContractsStartTest.cs => StartTest.cs} | 10 ++++------ .../{MarketTests => Utils}/ChainMonitor.cs | 2 +- .../MarketplaceAutoBootstrapDistTest.cs | 8 ++++---- 8 files changed, 26 insertions(+), 23 deletions(-) rename Tests/CodexReleaseTests/MarketTests/{ContractFailedTest.cs => FailTest.cs} (93%) rename Tests/CodexReleaseTests/MarketTests/{ContractSuccessfulTest.cs => FinishTest.cs} (92%) rename Tests/CodexReleaseTests/MarketTests/{MultipleContractsTest.cs => SequentialContracts.cs} (93%) rename Tests/CodexReleaseTests/MarketTests/{ContractsStartTest.cs => StartTest.cs} (88%) rename Tests/CodexReleaseTests/{MarketTests => Utils}/ChainMonitor.cs (98%) rename Tests/CodexReleaseTests/{MarketTests => Utils}/MarketplaceAutoBootstrapDistTest.cs (98%) diff --git a/Tests/CodexReleaseTests/DataTests/DecodeTest.cs b/Tests/CodexReleaseTests/DataTests/DecodeTest.cs index f6a1f309..1aaf742f 100644 --- a/Tests/CodexReleaseTests/DataTests/DecodeTest.cs +++ b/Tests/CodexReleaseTests/DataTests/DecodeTest.cs @@ -1,5 +1,5 @@ using System.Security.Cryptography; -using CodexReleaseTests.MarketTests; +using CodexReleaseTests.Utils; using Nethereum.JsonRpc.Client; using NUnit.Framework; using Utils; diff --git a/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs b/Tests/CodexReleaseTests/MarketTests/FailTest.cs similarity index 93% rename from Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs rename to Tests/CodexReleaseTests/MarketTests/FailTest.cs index d340ba26..a8b39464 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractFailedTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FailTest.cs @@ -1,10 +1,11 @@ using CodexClient; +using CodexReleaseTests.Utils; using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests { - public class ContractFailedTest : MarketplaceAutoBootstrapDistTest + public class FailTest : MarketplaceAutoBootstrapDistTest { protected override int NumberOfHosts => 4; protected override int NumberOfClients => 1; @@ -13,8 +14,7 @@ namespace CodexReleaseTests.MarketTests private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] - [Ignore("Disabled for now: Test is unstable.")] - public void ContractFailed() + public void Fail() { var hosts = StartHosts(); var client = StartClients().Single(); diff --git a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs similarity index 92% rename from Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs rename to Tests/CodexReleaseTests/MarketTests/FinishTest.cs index 9a816a9f..993fb526 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs @@ -1,4 +1,5 @@ using CodexClient; +using CodexReleaseTests.Utils; using NUnit.Framework; using Utils; @@ -6,9 +7,9 @@ namespace CodexReleaseTests.MarketTests { [TestFixture(5, 3, 1)] [TestFixture(10, 20, 10)] - public class ContractSuccessfulTest : MarketplaceAutoBootstrapDistTest + public class FinishTest : MarketplaceAutoBootstrapDistTest { - public ContractSuccessfulTest(int hosts, int slots, int tolerance) + public FinishTest(int hosts, int slots, int tolerance) { this.hosts = hosts; this.slots = slots; @@ -27,7 +28,8 @@ namespace CodexReleaseTests.MarketTests protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] - public void ContractSuccessful() + [Repeat(16)] + public void Finish() { var hosts = StartHosts(); var client = StartClients().Single(); diff --git a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs index 7d9bdfed..2877108d 100644 --- a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs @@ -1,4 +1,5 @@ using CodexClient; +using CodexReleaseTests.Utils; using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; @@ -49,6 +50,7 @@ namespace CodexReleaseTests.MarketTests { var hosts = StartHosts().ToList(); var client = StartClients().Single(); + StartValidator(); var contract = CreateStorageRequest(client); contract.WaitForStorageContractStarted(); @@ -178,7 +180,7 @@ namespace CodexReleaseTests.MarketTests MinRequiredNumberOfNodes = Slots, NodeFailureTolerance = Tolerance, PricePerBytePerSecond = 10.TstWei(), - ProofProbability = 20, + ProofProbability = 1, // One proof every period. Free slot as quickly as possible. CollateralPerByte = 1.TstWei() }); } diff --git a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs similarity index 93% rename from Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs rename to Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs index b37a9af2..b10e34ca 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs @@ -1,14 +1,15 @@ using CodexClient; using CodexPlugin; +using CodexReleaseTests.Utils; using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests { [TestFixture(12, 48, 12)] - public class MultipleContractsTest : MarketplaceAutoBootstrapDistTest + public class SequentialContracts : MarketplaceAutoBootstrapDistTest { - public MultipleContractsTest(int hosts, int slots, int tolerance) + public SequentialContracts(int hosts, int slots, int tolerance) { this.hosts = hosts; this.slots = slots; @@ -28,7 +29,7 @@ namespace CodexReleaseTests.MarketTests [Test] [Combinatorial] - public void MultipleContractGenerations( + public void Sequential( [Values(10)] int numGenerations) { var hosts = StartHosts(); @@ -88,7 +89,7 @@ namespace CodexReleaseTests.MarketTests MinRequiredNumberOfNodes = (uint)slots, NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, - ProofProbability = 1000, + ProofProbability = 10000, CollateralPerByte = 1.TstWei() }); } diff --git a/Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs b/Tests/CodexReleaseTests/MarketTests/StartTest.cs similarity index 88% rename from Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs rename to Tests/CodexReleaseTests/MarketTests/StartTest.cs index 9099e59d..70d90552 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractsStartTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/StartTest.cs @@ -1,11 +1,12 @@ using CodexClient; +using CodexReleaseTests.Utils; using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests { [TestFixture] - public class ContractsStartTest : MarketplaceAutoBootstrapDistTest + public class StartTest : MarketplaceAutoBootstrapDistTest { private const int FilesizeMb = 10; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); @@ -16,11 +17,8 @@ namespace CodexReleaseTests.MarketTests protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] - [Combinatorial] - public void ContractStarts( - [Values(1, 2, 3)] int rerunA, - [Values(1, 2, 3)] int rerunB, - [Values(1, 2, 3)] int rerunC) + [Repeat(16)] + public void Start() { var hosts = StartHosts(); var client = StartClients().Single(); diff --git a/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs b/Tests/CodexReleaseTests/Utils/ChainMonitor.cs similarity index 98% rename from Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs rename to Tests/CodexReleaseTests/Utils/ChainMonitor.cs index f9e6b9d6..d6a4a4d8 100644 --- a/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs +++ b/Tests/CodexReleaseTests/Utils/ChainMonitor.cs @@ -2,7 +2,7 @@ using CodexContractsPlugin.ChainMonitor; using Logging; -namespace CodexReleaseTests.MarketTests +namespace CodexReleaseTests.Utils { public class ChainMonitor { diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/Utils/MarketplaceAutoBootstrapDistTest.cs similarity index 98% rename from Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs rename to Tests/CodexReleaseTests/Utils/MarketplaceAutoBootstrapDistTest.cs index 74a1b6d5..72442098 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/Utils/MarketplaceAutoBootstrapDistTest.cs @@ -9,7 +9,7 @@ using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; -namespace CodexReleaseTests.MarketTests +namespace CodexReleaseTests.Utils { public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest { @@ -45,7 +45,7 @@ namespace CodexReleaseTests.MarketTests protected TimeSpan GetPeriodDuration() { var config = GetContracts().Deployment.Config; - return TimeSpan.FromSeconds(((double)config.Proofs.Period)); + return TimeSpan.FromSeconds(config.Proofs.Period); } protected abstract int NumberOfHosts { get; } @@ -320,7 +320,7 @@ namespace CodexReleaseTests.MarketTests private DateTime GetContractOnChainSubmittedUtc(IStoragePurchaseContract contract) { - return Time.Retry(() => + return Time.Retry(() => { var events = GetContracts().GetEvents(GetTestRunTimeRange()); var submitEvent = events.GetStorageRequests().SingleOrDefault(e => e.RequestId.ToHex(false) == contract.PurchaseId); @@ -395,7 +395,7 @@ namespace CodexReleaseTests.MarketTests // failed a sufficient number of proofs. float n = requiredNumMissedProofs; - return gracePeriod + (periodDuration * n * GetDowntimeFactor(config)); + return gracePeriod + periodDuration * n * GetDowntimeFactor(config); } private float GetDowntimeFactor(MarketplaceConfig config) From d3642ffb4e387fd749a79a9087481e87dc6e5b56 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Fri, 30 May 2025 09:57:21 +0200 Subject: [PATCH 64/69] Improved rerun that support parallelism. Disable fail and repair tests until fixed/implemented. --- .../CodexReleaseTests/MarketTests/FailTest.cs | 19 +++++++++++-------- .../MarketTests/FinishTest.cs | 6 ++++-- .../MarketTests/RepairTest.cs | 4 +++- .../MarketTests/StartTest.cs | 6 ++++-- Tests/CodexReleaseTests/Utils/ChainMonitor.cs | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Tests/CodexReleaseTests/MarketTests/FailTest.cs b/Tests/CodexReleaseTests/MarketTests/FailTest.cs index a8b39464..a0a818ae 100644 --- a/Tests/CodexReleaseTests/MarketTests/FailTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FailTest.cs @@ -11,10 +11,13 @@ namespace CodexReleaseTests.MarketTests protected override int NumberOfClients => 1; protected override ByteSize HostAvailabilitySize => 1.GB(); protected override TimeSpan HostAvailabilityMaxDuration => TimeSpan.FromDays(1.0); - private readonly TestToken pricePerBytePerSecond = 10.TstWei(); + [Ignore("Slots are never freed because proofs are never marked as missing. Issue: https://github.com/codex-storage/nim-codex/issues/1153")] [Test] - public void Fail() + [Combinatorial] + public void Fail( + [Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun + ) { var hosts = StartHosts(); var client = StartClients().Single(); @@ -37,11 +40,11 @@ namespace CodexReleaseTests.MarketTests private void WaitForSlotFreedEvents() { - Log(nameof(WaitForSlotFreedEvents)); - var start = DateTime.UtcNow; var timeout = CalculateContractFailTimespan(); + Log($"{nameof(WaitForSlotFreedEvents)} timeout: {Time.FormatDuration(timeout)}"); + while (DateTime.UtcNow < start + timeout) { var events = GetContracts().GetEvents(GetTestRunTimeRange()); @@ -61,13 +64,13 @@ namespace CodexReleaseTests.MarketTests var cid = client.UploadFile(GenerateTestFile(5.MB())); return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { - Duration = TimeSpan.FromHours(1.0), - Expiry = TimeSpan.FromHours(0.2), + Duration = HostAvailabilityMaxDuration / 2, + Expiry = TimeSpan.FromMinutes(5.0), MinRequiredNumberOfNodes = (uint)NumberOfHosts, NodeFailureTolerance = (uint)(NumberOfHosts / 2), - PricePerBytePerSecond = pricePerBytePerSecond, + PricePerBytePerSecond = 100.TstWei(), ProofProbability = 1, // Require a proof every period - CollateralPerByte = 1.Tst() + CollateralPerByte = 1.TstWei() }); } } diff --git a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs index 993fb526..0d10cc93 100644 --- a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs @@ -28,8 +28,10 @@ namespace CodexReleaseTests.MarketTests protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] - [Repeat(16)] - public void Finish() + [Combinatorial] + public void Finish( + [Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun + ) { var hosts = StartHosts(); var client = StartClients().Single(); diff --git a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs index 2877108d..2d8fa054 100644 --- a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs @@ -42,10 +42,12 @@ namespace CodexReleaseTests.MarketTests #endregion - [Ignore("Test is ready. Waiting for repair implementation.")] + [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")] [Test] [Combinatorial] public void RollingRepairSingleFailure( + [Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun, [Values(10)] int numFailures) { var hosts = StartHosts().ToList(); diff --git a/Tests/CodexReleaseTests/MarketTests/StartTest.cs b/Tests/CodexReleaseTests/MarketTests/StartTest.cs index 70d90552..2ba12d6a 100644 --- a/Tests/CodexReleaseTests/MarketTests/StartTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/StartTest.cs @@ -17,8 +17,10 @@ namespace CodexReleaseTests.MarketTests protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] - [Repeat(16)] - public void Start() + [Combinatorial] + public void Start( + [Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun + ) { var hosts = StartHosts(); var client = StartClients().Single(); diff --git a/Tests/CodexReleaseTests/Utils/ChainMonitor.cs b/Tests/CodexReleaseTests/Utils/ChainMonitor.cs index d6a4a4d8..8b4bd816 100644 --- a/Tests/CodexReleaseTests/Utils/ChainMonitor.cs +++ b/Tests/CodexReleaseTests/Utils/ChainMonitor.cs @@ -44,6 +44,7 @@ namespace CodexReleaseTests.Utils var state = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, doProofPeriodMonitoring: true); Thread.Sleep(updateInterval); + log.Log("Chain monitoring started"); while (!cts.IsCancellationRequested) { try From f344facb642a86f357496dc9ad6ace66eb7ec884 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 2 Jun 2025 13:32:03 +0200 Subject: [PATCH 65/69] Applies purchase-paramters type to ensure slot sizes --- Framework/Utils/ByteSize.cs | 15 +++++ .../CodexPlugin/CodexDockerImage.cs | 2 +- .../MarketTests/FinishTest.cs | 15 ++--- .../MarketTests/RepairTest.cs | 33 +++------ .../MarketTests/SequentialContracts.cs | 17 ++--- .../MarketTests/StartTest.cs | 14 ++-- .../CodexReleaseTests/Utils/PurchaseParams.cs | 67 +++++++++++++++++++ 7 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 Tests/CodexReleaseTests/Utils/PurchaseParams.cs diff --git a/Framework/Utils/ByteSize.cs b/Framework/Utils/ByteSize.cs index 9b0f7c8a..b66c952c 100644 --- a/Framework/Utils/ByteSize.cs +++ b/Framework/Utils/ByteSize.cs @@ -25,6 +25,21 @@ return new ByteSize(Convert.ToInt64(result)); } + public int DivUp(ByteSize div) + { + var d = div.SizeInBytes; + var remaining = SizeInBytes; + var result = 0; + while (remaining > d) + { + remaining -= d; + result++; + } + + if (remaining > 0) result++; + return result; + } + public override bool Equals(object? obj) { return obj is ByteSize size && SizeInBytes == size.SizeInBytes; diff --git a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index 01ffd4d7..57619f0b 100644 --- a/ProjectPlugins/CodexPlugin/CodexDockerImage.cs +++ b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs @@ -2,7 +2,7 @@ { public class CodexDockerImage { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-28a83db-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.3-dist-tests"; public static string Override { get; set; } = string.Empty; diff --git a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs index 0d10cc93..4f465c7f 100644 --- a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs @@ -12,19 +12,16 @@ namespace CodexReleaseTests.MarketTests public FinishTest(int hosts, int slots, int tolerance) { this.hosts = hosts; - this.slots = slots; - this.tolerance = tolerance; + purchaseParams = new PurchaseParams(slots, tolerance, uploadFilesize: 10.MB()); } - private const int FilesizeMb = 10; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); private readonly int hosts; - private readonly int slots; - private readonly int tolerance; + private readonly PurchaseParams purchaseParams; protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 1; - protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); + protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(5.1); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] @@ -53,14 +50,14 @@ namespace CodexReleaseTests.MarketTests private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { - var cid = client.UploadFile(GenerateTestFile(FilesizeMb.MB())); + var cid = client.UploadFile(GenerateTestFile(purchaseParams.UploadFilesize)); var config = GetContracts().Deployment.Config; return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = (uint)slots, - NodeFailureTolerance = (uint)tolerance, + MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes, + NodeFailureTolerance = (uint)purchaseParams.Tolerance, PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 20, CollateralPerByte = 100.TstWei() diff --git a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs index 2d8fa054..c515f639 100644 --- a/Tests/CodexReleaseTests/MarketTests/RepairTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/RepairTest.cs @@ -11,35 +11,22 @@ namespace CodexReleaseTests.MarketTests { #region Setup - private readonly ByteSize Filesize; - private readonly uint Slots; - private readonly uint Tolerance; - private readonly ByteSize EncodedFilesize; - private readonly ByteSize SlotSize; + private readonly PurchaseParams purchaseParams = new PurchaseParams( + nodes: 4, + tolerance: 2, + uploadFilesize: 32.MB() + ); public RepairTest() { - Filesize = 32.MB(); - Slots = 4; - Tolerance = 2; - - EncodedFilesize = new ByteSize(Filesize.SizeInBytes * (Slots / Tolerance)); - SlotSize = new ByteSize(EncodedFilesize.SizeInBytes / Slots); - Assert.That(IsPowerOfTwo(SlotSize)); - Assert.That(Slots, Is.LessThan(NumberOfHosts)); + Assert.That(purchaseParams.Nodes, Is.LessThan(NumberOfHosts)); } protected override int NumberOfHosts => 5; protected override int NumberOfClients => 1; - protected override ByteSize HostAvailabilitySize => SlotSize.Multiply(1.1); // Each host can hold 1 slot. + protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(1.1); // Each host can hold 1 slot. protected override TimeSpan HostAvailabilityMaxDuration => TimeSpan.FromDays(5.0); - private static bool IsPowerOfTwo(ByteSize size) - { - var x = size.SizeInBytes; - return (x != 0) && ((x & (x - 1)) == 0); - } - #endregion [Ignore("Test is ready. Waiting for repair implementation. " + @@ -173,14 +160,14 @@ namespace CodexReleaseTests.MarketTests private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { - var cid = client.UploadFile(GenerateTestFile(Filesize)); + var cid = client.UploadFile(GenerateTestFile(purchaseParams.UploadFilesize)); var config = GetContracts().Deployment.Config; return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { Duration = HostAvailabilityMaxDuration / 2, Expiry = TimeSpan.FromMinutes(10.0), - MinRequiredNumberOfNodes = Slots, - NodeFailureTolerance = Tolerance, + MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes, + NodeFailureTolerance = (uint)purchaseParams.Tolerance, PricePerBytePerSecond = 10.TstWei(), ProofProbability = 1, // One proof every period. Free slot as quickly as possible. CollateralPerByte = 1.TstWei() diff --git a/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs index b10e34ca..f8724f3d 100644 --- a/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs +++ b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs @@ -12,18 +12,15 @@ namespace CodexReleaseTests.MarketTests public SequentialContracts(int hosts, int slots, int tolerance) { this.hosts = hosts; - this.slots = slots; - this.tolerance = tolerance; + purchaseParams = new PurchaseParams(slots, tolerance, 10.MB()); } - private const int FilesizeMb = 10; private readonly int hosts; - private readonly int slots; - private readonly int tolerance; + private readonly PurchaseParams purchaseParams; protected override int NumberOfHosts => hosts; protected override int NumberOfClients => 8; - protected override ByteSize HostAvailabilitySize => (1000 * FilesizeMb).MB(); + protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(100.0); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); @@ -80,14 +77,14 @@ namespace CodexReleaseTests.MarketTests private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { - var cid = client.UploadFile(GenerateTestFile(FilesizeMb.MB())); + var cid = client.UploadFile(GenerateTestFile(purchaseParams.UploadFilesize)); var config = GetContracts().Deployment.Config; return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = (uint)slots, - NodeFailureTolerance = (uint)tolerance, + MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes, + NodeFailureTolerance = (uint)purchaseParams.Tolerance, PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 10000, CollateralPerByte = 1.TstWei() @@ -107,7 +104,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan Get8TimesConfiguredPeriodDuration() { var config = GetContracts().Deployment.Config; - return TimeSpan.FromSeconds(((double)config.Proofs.Period) * 8.0); + return TimeSpan.FromSeconds(config.Proofs.Period * 8.0); } } } diff --git a/Tests/CodexReleaseTests/MarketTests/StartTest.cs b/Tests/CodexReleaseTests/MarketTests/StartTest.cs index 2ba12d6a..c64aef2a 100644 --- a/Tests/CodexReleaseTests/MarketTests/StartTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/StartTest.cs @@ -8,12 +8,16 @@ namespace CodexReleaseTests.MarketTests [TestFixture] public class StartTest : MarketplaceAutoBootstrapDistTest { - private const int FilesizeMb = 10; + private readonly PurchaseParams purchaseParams = new PurchaseParams( + nodes: 3, + tolerance: 1, + uploadFilesize: 10.MB() + ); private readonly TestToken pricePerBytePerSecond = 10.TstWei(); protected override int NumberOfHosts => 5; protected override int NumberOfClients => 1; - protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); + protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(10.0); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] @@ -36,14 +40,14 @@ namespace CodexReleaseTests.MarketTests private IStoragePurchaseContract CreateStorageRequest(ICodexNode client) { - var cid = client.UploadFile(GenerateTestFile(FilesizeMb.MB())); + var cid = client.UploadFile(GenerateTestFile(purchaseParams.UploadFilesize)); var config = GetContracts().Deployment.Config; return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid) { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = 3, - NodeFailureTolerance = 1, + MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes, + NodeFailureTolerance = (uint)purchaseParams.Tolerance, PricePerBytePerSecond = pricePerBytePerSecond, ProofProbability = 20, CollateralPerByte = 100.TstWei() diff --git a/Tests/CodexReleaseTests/Utils/PurchaseParams.cs b/Tests/CodexReleaseTests/Utils/PurchaseParams.cs new file mode 100644 index 00000000..483d6f36 --- /dev/null +++ b/Tests/CodexReleaseTests/Utils/PurchaseParams.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using Utils; + +namespace CodexReleaseTests.Utils +{ + public class PurchaseParams + { + private readonly ByteSize blockSize = 64.KB(); + + public PurchaseParams(int nodes, int tolerance, ByteSize uploadFilesize) + { + Nodes = nodes; + Tolerance = tolerance; + UploadFilesize = uploadFilesize; + + EncodedDatasetSize = CalculateEncodedDatasetSize(); + SlotSize = CalculateSlotSize(); + + Assert.That(IsPowerOfTwo(SlotSize)); + } + + public int Nodes { get; } + public int Tolerance { get; } + public ByteSize UploadFilesize { get; } + public ByteSize EncodedDatasetSize { get; } + public ByteSize SlotSize { get; } + + private ByteSize CalculateSlotSize() + { + // encoded dataset is divided over the nodes. + // then each slot is rounded up to the nearest power-of-two blocks. + var numBlocks = EncodedDatasetSize.DivUp(blockSize); + var numSlotBlocks = 1 + ((numBlocks - 1) / Nodes); // round-up div. + + // Next power of two: + var numSlotBlocksPow2 = NextPowerOf2(numSlotBlocks); + return new ByteSize(blockSize.SizeInBytes * numSlotBlocksPow2); + } + + private ByteSize CalculateEncodedDatasetSize() + { + var numBlocks = UploadFilesize.DivUp(blockSize); + + var ecK = Nodes - Tolerance; + var ecM = Tolerance; + + // for each K blocks, we generate M parity blocks + var numParityBlocks = (numBlocks / ecK) * ecM; + var totalBlocks = numBlocks + numParityBlocks; + + return new ByteSize(blockSize.SizeInBytes * totalBlocks); + } + + private int NextPowerOf2(int n) + { + n = n - 1; + var lg = Convert.ToInt32(Math.Round(Math.Log2(Convert.ToDouble(n)))); + return 1 << (lg + 1); + } + + private static bool IsPowerOfTwo(ByteSize size) + { + var x = size.SizeInBytes; + return (x != 0) && ((x & (x - 1)) == 0); + } + } +} From 104e2ec929ad4ad84a99d8ed4c2b0a1e63cfbe45 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 2 Jun 2025 18:26:39 +0200 Subject: [PATCH 66/69] new marketplace address --- Tools/TraceContract/Config.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs index 1677e5e4..f9a955f3 100644 --- a/Tools/TraceContract/Config.cs +++ b/Tools/TraceContract/Config.cs @@ -4,7 +4,7 @@ { public string RpcEndpoint { get; } = "https://rpc.testnet.codex.storage"; public int GethPort { get; } = 443; - public string MarketplaceAddress { get; } = "0xDB2908d724a15d05c0B6B8e8441a8b36E67476d3"; + public string MarketplaceAddress { get; } = "0x7c7a749DE7156305E55775e7Ab3931abd6f7300E"; public string TokenAddress { get; } = "0x34a22f3911De437307c6f4485931779670f78764"; public string Abi { get; } = @"[{""inputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":""configuration"",""type"":""tuple""},{""internalType"":""contract IERC20"",""name"":""token_"",""type"":""address""},{""internalType"":""contract IGroth16Verifier"",""name"":""verifier"",""type"":""address""}],""stateMutability"":""nonpayable"",""type"":""constructor""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""ProofSubmitted"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestCancelled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFailed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""RequestFulfilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFilled"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":true,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""indexed"":false,""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""name"":""SlotFreed"",""type"":""event""},{""anonymous"":false,""inputs"":[{""indexed"":false,""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""indexed"":false,""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""indexed"":false,""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""}],""name"":""StorageRequested"",""type"":""event""},{""inputs"":[],""name"":""config"",""outputs"":[{""components"":[{""components"":[{""internalType"":""uint8"",""name"":""repairRewardPercentage"",""type"":""uint8""},{""internalType"":""uint8"",""name"":""maxNumberOfSlashes"",""type"":""uint8""},{""internalType"":""uint16"",""name"":""slashCriterion"",""type"":""uint16""},{""internalType"":""uint8"",""name"":""slashPercentage"",""type"":""uint8""}],""internalType"":""struct CollateralConfig"",""name"":""collateral"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""period"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""timeout"",""type"":""uint256""},{""internalType"":""uint8"",""name"":""downtime"",""type"":""uint8""},{""internalType"":""string"",""name"":""zkeyHash"",""type"":""string""}],""internalType"":""struct ProofConfig"",""name"":""proofs"",""type"":""tuple""}],""internalType"":""struct MarketplaceConfig"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""fillSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""freeSlot"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getActiveSlot"",""outputs"":[{""components"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""slotIndex"",""type"":""uint256""}],""internalType"":""struct Marketplace.ActiveSlot"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getChallenge"",""outputs"":[{""internalType"":""bytes32"",""name"":"""",""type"":""bytes32""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""getHost"",""outputs"":[{""internalType"":""address"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""getPointer"",""outputs"":[{""internalType"":""uint8"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""getRequest"",""outputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":"""",""type"":""tuple""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""isProofRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""},{""internalType"":""Periods.Period"",""name"":""period"",""type"":""uint256""}],""name"":""markProofAsMissing"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""missingProofs"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""myRequests"",""outputs"":[{""internalType"":""RequestId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[],""name"":""mySlots"",""outputs"":[{""internalType"":""SlotId[]"",""name"":"""",""type"":""bytes32[]""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestEnd"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestExpiry"",""outputs"":[{""internalType"":""uint256"",""name"":"""",""type"":""uint256""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""requestState"",""outputs"":[{""internalType"":""enum RequestState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""components"":[{""internalType"":""address"",""name"":""client"",""type"":""address""},{""components"":[{""internalType"":""uint64"",""name"":""slots"",""type"":""uint64""},{""internalType"":""uint256"",""name"":""slotSize"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""duration"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""proofProbability"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""reward"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""collateral"",""type"":""uint256""},{""internalType"":""uint64"",""name"":""maxSlotLoss"",""type"":""uint64""}],""internalType"":""struct Ask"",""name"":""ask"",""type"":""tuple""},{""components"":[{""internalType"":""string"",""name"":""cid"",""type"":""string""},{""internalType"":""bytes32"",""name"":""merkleRoot"",""type"":""bytes32""}],""internalType"":""struct Content"",""name"":""content"",""type"":""tuple""},{""internalType"":""uint256"",""name"":""expiry"",""type"":""uint256""},{""internalType"":""bytes32"",""name"":""nonce"",""type"":""bytes32""}],""internalType"":""struct Request"",""name"":""request"",""type"":""tuple""}],""name"":""requestStorage"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""slotId"",""type"":""bytes32""}],""name"":""slotState"",""outputs"":[{""internalType"":""enum SlotState"",""name"":"""",""type"":""uint8""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""a"",""type"":""tuple""},{""components"":[{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""x"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""real"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""imag"",""type"":""uint256""}],""internalType"":""struct Fp2Element"",""name"":""y"",""type"":""tuple""}],""internalType"":""struct G2Point"",""name"":""b"",""type"":""tuple""},{""components"":[{""internalType"":""uint256"",""name"":""x"",""type"":""uint256""},{""internalType"":""uint256"",""name"":""y"",""type"":""uint256""}],""internalType"":""struct G1Point"",""name"":""c"",""type"":""tuple""}],""internalType"":""struct Groth16Proof"",""name"":""proof"",""type"":""tuple""}],""name"":""submitProof"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""},{""inputs"":[],""name"":""token"",""outputs"":[{""internalType"":""contract IERC20"",""name"":"""",""type"":""address""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""SlotId"",""name"":""id"",""type"":""bytes32""}],""name"":""willProofBeRequired"",""outputs"":[{""internalType"":""bool"",""name"":"""",""type"":""bool""}],""stateMutability"":""view"",""type"":""function""},{""inputs"":[{""internalType"":""RequestId"",""name"":""requestId"",""type"":""bytes32""}],""name"":""withdrawFunds"",""outputs"":[],""stateMutability"":""nonpayable"",""type"":""function""}]"; From ddc6599b6df29436d83d38e214139ce4c1f9831f Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 2 Jun 2025 18:51:36 +0200 Subject: [PATCH 67/69] adjust test parameters --- Tests/CodexReleaseTests/MarketTests/FinishTest.cs | 2 +- .../MarketTests/SequentialContracts.cs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs index 4f465c7f..3aff5b42 100644 --- a/Tests/CodexReleaseTests/MarketTests/FinishTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/FinishTest.cs @@ -71,7 +71,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() / 2; + return Get8TimesConfiguredPeriodDuration(); } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs index f8724f3d..383093cf 100644 --- a/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs +++ b/Tests/CodexReleaseTests/MarketTests/SequentialContracts.cs @@ -6,7 +6,7 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture(12, 48, 12)] + [TestFixture(10, 20, 5)] public class SequentialContracts : MarketplaceAutoBootstrapDistTest { public SequentialContracts(int hosts, int slots, int tolerance) @@ -19,7 +19,7 @@ namespace CodexReleaseTests.MarketTests private readonly PurchaseParams purchaseParams; protected override int NumberOfHosts => hosts; - protected override int NumberOfClients => 8; + protected override int NumberOfClients => 6; protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(100.0); protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); @@ -35,7 +35,14 @@ namespace CodexReleaseTests.MarketTests for (var i = 0; i < numGenerations; i++) { Log("Generation: " + i); - Generation(clients, hosts); + try + { + Generation(clients, hosts); + } + catch (Exception ex) + { + Assert.Fail($"Failed at generation {i} with exception {ex}"); + } } Thread.Sleep(TimeSpan.FromSeconds(12.0)); From 577f9be76c58e34c311119e002b5e648e09acc4a Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 3 Jun 2025 11:34:34 +0200 Subject: [PATCH 68/69] updates host addresses --- Tools/TraceContract/Config.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs index f9a955f3..3b49a77a 100644 --- a/Tools/TraceContract/Config.cs +++ b/Tools/TraceContract/Config.cs @@ -39,16 +39,16 @@ public Dictionary LogReplacements = new() { - { "0xa1f988fBa23EFd5fA36F4c1a2D1E3c83e25bee4e", "codex 01" }, - { "0xa26a91310F9f2987AA7e0b1ca70e5C474c88ed34", "codex 02" }, - { "0x0CDC9d2D375300C46E13a679cD9eA5299A4FAc74", "codex 03" }, - { "0x7AF1a49A4a52e4bCe3789Ce3d43ff8AD8c8F2118", "codex 04" }, - { "0xfbbEB320c6c775f6565c7bcC732b2813Dd6E0cd3", "codex 05" }, - { "0x4A904CA0998B643eb42d4ae190a5821A4ac51E68", "codex 06" }, - { "0x2b8Ea47d0966B26DEec485c0fCcF0D1A8b52A0e8", "codex 07" }, - { "0x78F90A61d9a2aA93B61A7503Cc2177fFEF379021", "codex 08" }, - { "0xE7EEb996B3c817cEd03d10cd64A1325DA33D92e7", "codex 09" }, - { "0xD25C7609e97F40b66E74c0FcEbeA06D09423CC7e", "codex 10" } + { "0x3620ec38d88e9f0cf7feceebf97864f27676aa3e", "codex-01" }, + { "0xd80dc50af2a826f2cddc13840d05aed4ee6536c3", "codex-02" }, + { "0x2d1cd0fa0c7e0d29e7b2482b9ff87d5e7b76b905", "codex-03" }, + { "0xd47063bb6e56c9a6edb7612d33ad7d49eeb55ee0", "codex-04" }, + { "0x069da63e29b12a3828984379fcbd7dd3ee3774aa", "codex-05" }, + { "0x43fcceb2a9ce4761ccaa4c9f8d390c7581c190aa", "codex-06" }, + { "0x1a30cef06dbbf8ec25062e4e8d22e8df292f5054", "codex-07" }, + { "0xe169b5dcbae9a7392072323aaf5a677a33d67ecd", "codex-08" }, + { "0x21f7428619ef9f53addc5dab6723c822a8a96b42", "codex-09" }, + { "0xf9bd20512de2d5ca0dcfd8d3cd08a2821917797a", "codex-10" } }; public string GetElasticSearchUsername() From 6ad001c74462412277447c77b29b403cda189b42 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 3 Jun 2025 13:34:39 +0200 Subject: [PATCH 69/69] Applies logreplace to console output as well --- Tools/TraceContract/Output.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Tools/TraceContract/Output.cs b/Tools/TraceContract/Output.cs index 255bbf3e..be89d3f6 100644 --- a/Tools/TraceContract/Output.cs +++ b/Tools/TraceContract/Output.cs @@ -28,21 +28,22 @@ namespace TraceContract public Output(ILog log, Input input, Config config) { + this.input = input; + this.config = config; + folder = config.GetOuputFolder(); Directory.CreateDirectory(folder); var filename = Path.Combine(folder, $"contract_{input.PurchaseId}"); var fileLog = new FileLog(filename); + log.Log($"Logging to '{filename}'"); + + this.log = new LogSplitter(fileLog, log); foreach (var pair in config.LogReplacements) { - fileLog.AddStringReplace(pair.Key, pair.Value); - fileLog.AddStringReplace(pair.Key.ToLowerInvariant(), pair.Value); + this.log.AddStringReplace(pair.Key, pair.Value); + this.log.AddStringReplace(pair.Key.ToLowerInvariant(), pair.Value); } - - log.Log($"Logging to '{filename}'"); - this.log = new LogSplitter(fileLog, log); - this.input = input; - this.config = config; } public void LogRequestCreated(RequestEvent requestEvent)