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/Framework/BlockchainUtils/BlockTimeFinder.cs b/Framework/BlockchainUtils/BlockTimeFinder.cs index a84cda3a..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); } @@ -101,10 +102,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."); 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/KubernetesWorkflow/LogHandler.cs b/Framework/KubernetesWorkflow/LogHandler.cs index af77ef75..b91a61f9 100644 --- a/Framework/KubernetesWorkflow/LogHandler.cs +++ b/Framework/KubernetesWorkflow/LogHandler.cs @@ -33,14 +33,17 @@ namespace KubernetesWorkflow sourceLog.Log(msg); LogFile.Write(msg); - LogFile.WriteRaw(description); + LogFile.Write(description); } public LogFile LogFile { get; } protected override void ProcessLine(string line) { - LogFile.WriteRaw(line); + if (line.Contains("Received JSON-RPC response")) return; + if (line.Contains("object field not marked with serialize, skipping")) return; + + 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/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 3b2c609c..6a34e848 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 GetBlockWithTransactions(ulong number) + { + return Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(number))); + } } } 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/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs index 1bb47912..5dfa0114 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/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs index 6decfde6..e2d8a11a 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -19,6 +19,7 @@ namespace CodexContractsPlugin SlotFreedEventDTO[] GetSlotFreedEvents(); SlotReservationsFullEventDTO[] GetSlotReservationsFullEvents(); ProofSubmittedEventDTO[] GetProofSubmittedEvents(); + ReserveSlotFunction[] GetReserveSlotCalls(); } public class CodexContractsEvents : ICodexContractsEvents @@ -99,6 +100,17 @@ namespace CodexContractsPlugin return events.Select(SetBlockOnEvent).ToArray(); } + public ReserveSlotFunction[] GetReserveSlotCalls() + { + var result = new List(); + gethNode.IterateFunctionCalls(BlockInterval, (b, fn) => + { + fn.Block = b; + result.Add(fn); + }); + return result.ToArray(); + } + private T SetBlockOnEvent(EventLog e) where T : IHasBlock { var result = e.Event; 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/CodexPlugin/CodexDockerImage.cs b/ProjectPlugins/CodexPlugin/CodexDockerImage.cs index bbb09c0b..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:latest-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:0.2.2-dist-tests"; public static string Override { get; set; } = string.Empty; diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index 26bf1f03..cfd0af37 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); + void IterateFunctionCalls(BlockInterval blockInterval, Action onCall) where TFunc : FunctionMessage, new(); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -183,6 +184,33 @@ namespace GethPlugin return StartInteraction().GetBlockForNumber(number); } + public BlockWithTransactions GetBlk(ulong 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) + { + if (t.IsTransactionForFunctionMessage()) + { + var func = t.DecodeTransactionToFunctionMessage(); + if (func != null) + { + var b = GetBlockForNumber(blkI); + onCall(b, func); + } + } + } + } + } + protected abstract NethereumInteraction StartInteraction(); } } 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/CodexReleaseTests/MarketTests/ChainMonitor.cs b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs new file mode 100644 index 00000000..f9e6b9d6 --- /dev/null +++ b/Tests/CodexReleaseTests/MarketTests/ChainMonitor.cs @@ -0,0 +1,78 @@ +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(Action onFailure) + { + cts = new CancellationTokenSource(); + worker = Task.Run(() => Worker(onFailure)); + } + + public void Stop() + { + cts.Cancel(); + worker.Wait(); + if (worker.Exception != null) throw worker.Exception; + } + + private void Worker(Action onFailure) + { + var state = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, doProofPeriodMonitoring: true); + Thread.Sleep(updateInterval); + + while (!cts.IsCancellationRequested) + { + try + { + UpdateChainState(state); + } + catch (Exception ex) + { + log.Error("Exception in chain monitor: " + ex); + onFailure(); + throw; + } + + 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 => r.MissedProofs.Length); + + 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..51f9b79b 100644 --- a/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/ContractSuccessfulTest.cs @@ -4,16 +4,30 @@ using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture] + [TestFixture(6, 3, 1)] + [TestFixture(6, 4, 2)] + [TestFixture(8, 5, 1)] + [TestFixture(8, 6, 1)] + [TestFixture(8, 6, 3)] 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(); + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; [Test] public void ContractSuccessful() @@ -26,9 +40,12 @@ namespace CodexReleaseTests.MarketTests request.WaitForStorageContractSubmitted(); AssertContractIsOnChain(request); - request.WaitForStorageContractStarted(); + WaitForContractStarted(request); AssertContractSlotsAreFilledByHosts(request, hosts); + Thread.Sleep(TimeSpan.FromSeconds(12.0)); + return; + request.WaitForStorageContractFinished(); AssertClientHasPaidForContract(pricePerBytePerSecond, client, request, hosts); @@ -44,11 +61,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() @@ -62,7 +76,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() / 2; + return Get8TimesConfiguredPeriodDuration() * 4; } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs index b209ccc4..39d2e1b3 100644 --- a/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MarketplaceAutoBootstrapDistTest.cs @@ -4,6 +4,7 @@ using CodexContractsPlugin.Marketplace; using CodexPlugin; using CodexTests; using GethPlugin; +using Logging; using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; @@ -21,7 +22,14 @@ namespace CodexReleaseTests.MarketTests { var geth = StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version); - handle = new MarketplaceHandle(geth, contracts); + var monitor = SetupChainMonitor(GetTestLog(), contracts, GetTestRunTimeRange().From); + handle = new MarketplaceHandle(geth, contracts, monitor); + } + + [TearDown] + public void TearDownMarketplace() + { + if (handle.ChainMonitor != null) handle.ChainMonitor.Stop(); } protected IGethNode GetGeth() @@ -44,6 +52,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() { @@ -106,6 +115,19 @@ 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(() => + { + log.Error("Failure in chain monitor. No chain updates after this point."); + //Assert.Fail("Failure in chain monitor."); + }); + return result; + } + private Retry GetBalanceAssertRetry() { return new Retry("AssertBalance", @@ -232,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($" - {c.Block.Utc} Host: {c.FromAddress} RequestId: {c.RequestId.ToHex()} SlotIndex: {c.SlotIndex}"); + } + } + } + private TestToken GetContractFinalCost(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts) { var fills = GetOnChainSlotFills(hosts); @@ -324,14 +368,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..df71eb3b 100644 --- a/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs +++ b/Tests/CodexReleaseTests/MarketTests/MultipleContractsTest.cs @@ -1,28 +1,55 @@ using CodexClient; +using CodexPlugin; using NUnit.Framework; using Utils; namespace CodexReleaseTests.MarketTests { - [TestFixture] + //[TestFixture(8, 3, 1)] + [TestFixture(8, 4, 1)] + //[TestFixture(10, 5, 1)] + //[TestFixture(10, 6, 1)] + //[TestFixture(10, 6, 3)] 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; - protected override int NumberOfClients => 3; - protected override ByteSize HostAvailabilitySize => (5 * FilesizeMb).MB(); - protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration(); + 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 => 8; + protected override ByteSize HostAvailabilitySize => (1000 * FilesizeMb).MB(); + protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12; private readonly TestToken pricePerBytePerSecond = 10.TstWei(); [Test] - [Ignore("TODO - Test where multiple successful contracts are run simultaenously")] - public void MultipleSuccessfulContracts() + [Combinatorial] + public void MultipleContractGenerations( + [Values(50)] int numGenerations) { var hosts = StartHosts(); var clients = StartClients(); - var requests = clients.Select(c => CreateStorageRequest(c)).ToArray(); + for (var i = 0; i < numGenerations; i++) + { + Log("Generation: " + i); + Generation(clients, hosts); + } + + Thread.Sleep(TimeSpan.FromSeconds(12.0)); + } + + private void Generation(ICodexNodeGroup clients, ICodexNodeGroup hosts) + { + var requests = All(clients.ToArray(), CreateStorageRequest); All(requests, r => { @@ -30,28 +57,32 @@ namespace CodexReleaseTests.MarketTests AssertContractIsOnChain(r); }); - All(requests, r => r.WaitForStorageContractStarted()); - All(requests, r => AssertContractSlotsAreFilledByHosts(r, hosts)); + All(requests, WaitForContractStarted); - 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); + // 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) + private void All(T[] items, Action action) { - foreach (var r in requests) action(r); + 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; + } + } + + 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) @@ -62,11 +93,11 @@ namespace CodexReleaseTests.MarketTests { Duration = GetContractDuration(), Expiry = GetContractExpiry(), - MinRequiredNumberOfNodes = (uint)NumberOfHosts, - NodeFailureTolerance = (uint)(NumberOfHosts / 2), + MinRequiredNumberOfNodes = (uint)slots, + NodeFailureTolerance = (uint)tolerance, PricePerBytePerSecond = pricePerBytePerSecond, - ProofProbability = 20, - CollateralPerByte = 1.Tst() + ProofProbability = 1, + CollateralPerByte = 1.TstWei() }); } @@ -77,7 +108,7 @@ namespace CodexReleaseTests.MarketTests private TimeSpan GetContractDuration() { - return Get8TimesConfiguredPeriodDuration() / 2; + return Get8TimesConfiguredPeriodDuration() * 4; } private TimeSpan Get8TimesConfiguredPeriodDuration() diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 93732405..4c6cb0af 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -137,7 +137,7 @@ namespace DistTestCore protected TimeRange GetTestRunTimeRange() { - return new TimeRange(lifecycle.TestStart, DateTime.UtcNow); + return new TimeRange(lifecycle.TestStartUtc, DateTime.UtcNow); } protected virtual void Initialize(FixtureLog fixtureLog) 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/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) diff --git a/Tools/TraceContract/ChainRequestTracker.cs b/Tools/TraceContract/ChainRequestTracker.cs new file mode 100644 index 00000000..2e313ae8 --- /dev/null +++ b/Tools/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/Tools/TraceContract/ChainTracer.cs b/Tools/TraceContract/ChainTracer.cs new file mode 100644 index 00000000..c678667a --- /dev/null +++ b/Tools/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/Tools/TraceContract/Config.cs b/Tools/TraceContract/Config.cs new file mode 100644 index 00000000..5cdce6e7 --- /dev/null +++ b/Tools/TraceContract/Config.cs @@ -0,0 +1,76 @@ +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""}]"; + + /// + /// 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 ElasticSearchUrl + { + get + { + return GetEnvVar("ES_HOST", "es_host"); + } + } + + public string[] StorageNodesKubernetesPodNames = [ + "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" } + }; + + public string GetElasticSearchUsername() + { + return GetEnvVar("ES_USERNAME", "username"); + } + + public string GetElasticSearchPassword() + { + 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); + if (string.IsNullOrEmpty(v)) return defaultValue; + return v; + } + } +} diff --git a/Tools/TraceContract/ElasticSearchLogDownloader.cs b/Tools/TraceContract/ElasticSearchLogDownloader.cs new file mode 100644 index 00000000..a55d85ca --- /dev/null +++ b/Tools/TraceContract/ElasticSearchLogDownloader.cs @@ -0,0 +1,253 @@ +using System.Text; +using Core; +using Logging; +using Utils; +using WebUtils; + +namespace TraceContract +{ + public class ElasticSearchLogDownloader + { + private readonly ILog log; + private readonly IPluginTools tools; + private readonly Config config; + + public ElasticSearchLogDownloader(ILog log, IPluginTools tools, Config config) + { + this.log = log; + this.tools = tools; + this.config = config; + } + + public void Download(LogFile targetFile, string podName, DateTime startUtc, DateTime endUtc) + { + try + { + DownloadLog(targetFile, podName, startUtc, endUtc); + } + catch (Exception ex) + { + log.Error("Failed to download log: " + ex); + } + } + + private void DownloadLog(LogFile targetFile, string podName, DateTime startUtc, DateTime endUtc) + { + log.Log($"Downloading log (from ElasticSearch) for pod '{podName}' within time range: " + + $"{startUtc.ToString("o")} - {endUtc.ToString("o")}"); + + var endpoint = CreateElasticSearchEndpoint(); + var queryTemplate = CreateQueryTemplate(podName, startUtc, endUtc); + + 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 podName, 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\": { \"pod_name\": \"\" } } ] } } }"; + return source + .Replace("", start) + .Replace("", end) + .Replace("", podName); + //.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 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); + } + + 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/Tools/TraceContract/Input.cs b/Tools/TraceContract/Input.cs new file mode 100644 index 00000000..a0c63a03 --- /dev/null +++ b/Tools/TraceContract/Input.cs @@ -0,0 +1,21 @@ +namespace TraceContract +{ + public class Input + { + public string PurchaseId + { + get + { + var v = Environment.GetEnvironmentVariable("PURCHASE_ID"); + if (!string.IsNullOrEmpty(v)) return v; + + return + // expired: + "a7fe97dc32216aba0cbe74b87beb3f919aa116090dd5e0d48085a1a6b0080e82"; + + // started: + //"066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4"; + } + } + } +} diff --git a/Tools/TraceContract/Output.cs b/Tools/TraceContract/Output.cs new file mode 100644 index 00000000..a5d09bb1 --- /dev/null +++ b/Tools/TraceContract/Output.cs @@ -0,0 +1,134 @@ +using System.IO.Compression; +using System.Numerics; +using CodexContractsPlugin.ChainMonitor; +using CodexContractsPlugin.Marketplace; +using Logging; +using Utils; + +namespace TraceContract +{ + public class Output + { + private class Entry + { + 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(); + 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 fileLog = new FileLog(filename); + files.Add(fileLog.FullFilename + ".log"); + 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); + this.input = input; + this.config = config; + } + + public void LogRequestCreated(RequestEvent requestEvent) + { + Add(requestEvent.Block.Utc, $"Storage request created: '{requestEvent.Request.Request.Id}'"); + } + + public void LogRequestCancelled(RequestEvent requestEvent) + { + Add(requestEvent.Block.Utc, "Expired"); + } + + public void LogRequestFailed(RequestEvent requestEvent) + { + Add(requestEvent.Block.Utc, "Failed"); + } + + public void LogRequestFinished(RequestEvent requestEvent) + { + Add(requestEvent.Block.Utc, "Finished"); + } + + public void LogRequestStarted(RequestEvent requestEvent) + { + Add(requestEvent.Block.Utc, "Started"); + } + + public void LogSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + Add(requestEvent.Block.Utc, $"Slot filled. Index: {slotIndex} Host: '{host}'"); + } + + public void LogSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + Add(requestEvent.Block.Utc, $"Slot freed. Index: {slotIndex}"); + } + + public void LogSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex) + { + Add(requestEvent.Block.Utc, $"Slot reservations full. Index: {slotIndex}"); + } + + public void LogReserveSlotCalls(ReserveSlotFunction[] reserveSlotFunctions) + { + foreach (var call in reserveSlotFunctions) LogReserveSlotCall(call); + } + + public void WriteContractEvents() + { + var sorted = entries.OrderBy(e => e.Utc).ToArray(); + 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}"); + } + + private void LogReserveSlotCall(ReserveSlotFunction call) + { + 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 new file mode 100644 index 00000000..d239373c --- /dev/null +++ b/Tools/TraceContract/Program.cs @@ -0,0 +1,100 @@ +using BlockchainUtils; +using CodexContractsPlugin; +using CodexContractsPlugin.Marketplace; +using Core; +using GethPlugin; +using Logging; +using Utils; + +namespace TraceContract +{ + public class Program + { + public static void Main(string[] args) + { + ProjectPlugin.Load(); + ProjectPlugin.Load(); + + var p = new Program(); + p.Run(); + } + + private readonly ILog log = new ConsoleLog(); + private readonly Input input = new(); + private readonly Config config = new(); + private readonly Output output; + + public Program() + { + output = new(log, input, config); + } + + private void Run() + { + try + { + TracePurchase(); + } + catch (Exception exc) + { + log.Error(exc.ToString()); + } + } + + private void TracePurchase() + { + Log("Setting up..."); + 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); + + Log("Packaging..."); + var zipFilename = output.Package(); + Log($"Saved to '{zipFilename}'"); + + 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 MarketplaceConfig(), + marketplaceAddress: config.MarketplaceAddress, + abi: config.Abi, + tokenAddress: config.TokenAddress + ); + return ci.WrapCodexContractsDeployment(geth, deployment); + } + + private void DownloadStorageNodeLogs(TimeRange requestTimeRange, IPluginTools tools) + { + var start = requestTimeRange.From - config.LogStartBeforeStorageContractStarts; + + foreach (var node in config.StorageNodesKubernetesPodNames) + { + Log($"Downloading logs from '{node}'..."); + + var targetFile = output.CreateNodeLogTargetFile(node); + var downloader = new ElasticSearchLogDownloader(log, tools, config); + downloader.Download(targetFile, node, start, requestTimeRange.To); + } + } + + private void Log(string msg) + { + log.Log(msg); + } + } +} diff --git a/Tools/TraceContract/TraceContract.csproj b/Tools/TraceContract/TraceContract.csproj new file mode 100644 index 00000000..51fd39c6 --- /dev/null +++ b/Tools/TraceContract/TraceContract.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index 9b1c0977..a10e32d6 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", "Tools\TraceContract\TraceContract.csproj", "{58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8}" +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 + {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 @@ -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} + {58CDACE0-8F8D-2BB7-EA3A-0CB6A994A7F8} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}