From 229519a091a2ff3f656e251da3bc07a88afcb408 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:11:36 +0200 Subject: [PATCH 1/5] feature: add initial version of a pathfinder for circles v2 --- .../TestConversionUtils.cs | 15 +- Circles.Index.Query.Tests/TestSelect.cs | 15 ++ Circles.Index.Rpc/Circles.Index.Rpc.csproj | 1 + Circles.Index.Rpc/CirclesRpcModule.cs | 12 + Circles.Index.Rpc/ICirclesRpcModule.cs | 6 + Circles.Index/Circles.Index.csproj | 4 +- Circles.Pathfinder/Circles.Pathfinder.csproj | 13 + Circles.Pathfinder/DTOs/FlowRequest.cs | 8 + Circles.Pathfinder/DTOs/MaxFlowResponse.cs | 13 + Circles.Pathfinder/DTOs/TransferPathStep.cs | 9 + Circles.Pathfinder/Data/LoadGraph.cs | 54 +++++ Circles.Pathfinder/Edges/CapacityEdge.cs | 18 ++ Circles.Pathfinder/Edges/Edge.cs | 13 + Circles.Pathfinder/Edges/FlowEdge.cs | 20 ++ Circles.Pathfinder/Edges/TrustEdge.cs | 11 + Circles.Pathfinder/Graphs/BalanceGraph.cs | 38 +++ Circles.Pathfinder/Graphs/CapacityGraph.cs | 63 +++++ Circles.Pathfinder/Graphs/FlowGraph.cs | 225 ++++++++++++++++++ Circles.Pathfinder/Graphs/GraphExtensions.cs | 119 +++++++++ Circles.Pathfinder/Graphs/GraphFactory.cs | 148 ++++++++++++ Circles.Pathfinder/Graphs/GraphTraversal.cs | 138 +++++++++++ Circles.Pathfinder/Graphs/IGraph.cs | 11 + Circles.Pathfinder/Graphs/TrustGraph.cs | 41 ++++ Circles.Pathfinder/IPathfinder.cs | 8 + Circles.Pathfinder/Nodes/AvatarNode.cs | 8 + Circles.Pathfinder/Nodes/BalanceNode.cs | 17 ++ Circles.Pathfinder/Nodes/Node.cs | 15 ++ Circles.Pathfinder/V2Pathfinder.cs | 209 ++++++++++++++++ Circles.sln | 6 + arm64.Dockerfile | 1 + x64.Dockerfile | 1 + x64.debug.Dockerfile | 2 + x64.debug.spaceneth.Dockerfile | 2 + 33 files changed, 1249 insertions(+), 15 deletions(-) create mode 100644 Circles.Pathfinder/Circles.Pathfinder.csproj create mode 100644 Circles.Pathfinder/DTOs/FlowRequest.cs create mode 100644 Circles.Pathfinder/DTOs/MaxFlowResponse.cs create mode 100644 Circles.Pathfinder/DTOs/TransferPathStep.cs create mode 100644 Circles.Pathfinder/Data/LoadGraph.cs create mode 100644 Circles.Pathfinder/Edges/CapacityEdge.cs create mode 100644 Circles.Pathfinder/Edges/Edge.cs create mode 100644 Circles.Pathfinder/Edges/FlowEdge.cs create mode 100644 Circles.Pathfinder/Edges/TrustEdge.cs create mode 100644 Circles.Pathfinder/Graphs/BalanceGraph.cs create mode 100644 Circles.Pathfinder/Graphs/CapacityGraph.cs create mode 100644 Circles.Pathfinder/Graphs/FlowGraph.cs create mode 100644 Circles.Pathfinder/Graphs/GraphExtensions.cs create mode 100644 Circles.Pathfinder/Graphs/GraphFactory.cs create mode 100644 Circles.Pathfinder/Graphs/GraphTraversal.cs create mode 100644 Circles.Pathfinder/Graphs/IGraph.cs create mode 100644 Circles.Pathfinder/Graphs/TrustGraph.cs create mode 100644 Circles.Pathfinder/IPathfinder.cs create mode 100644 Circles.Pathfinder/Nodes/AvatarNode.cs create mode 100644 Circles.Pathfinder/Nodes/BalanceNode.cs create mode 100644 Circles.Pathfinder/Nodes/Node.cs create mode 100644 Circles.Pathfinder/V2Pathfinder.cs diff --git a/Circles.Index.Query.Tests/TestConversionUtils.cs b/Circles.Index.Query.Tests/TestConversionUtils.cs index b517207..d49d085 100644 --- a/Circles.Index.Query.Tests/TestConversionUtils.cs +++ b/Circles.Index.Query.Tests/TestConversionUtils.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Circles.Index.Utils; namespace Circles.Index.Query.Tests; @@ -37,18 +38,6 @@ public class TestConversionUtils [Test] public void TestConvertCirclesToStaticCircles() { - var inflation = 0.07m; - var year = 4; - var baseAmount = 8m; - var inflatedAmount = baseAmount; - for (int i = 0; i < year; i++) - { - inflatedAmount *= 1 + inflation; - } - - - - decimal circlesBalance = 20; - decimal staticCirclesBalance = ConversionUtils.CirclesToStaticCircles(circlesBalance, DateTime.Now); + decimal staticCirclesBalance = ConversionUtils.CirclesToCrc(0.13151319940322485m); } } \ No newline at end of file diff --git a/Circles.Index.Query.Tests/TestSelect.cs b/Circles.Index.Query.Tests/TestSelect.cs index 8ffaa67..4de85ec 100644 --- a/Circles.Index.Query.Tests/TestSelect.cs +++ b/Circles.Index.Query.Tests/TestSelect.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -368,4 +369,18 @@ public void JsonSerialization_Deserialization_Complex() Assert.That(deserializedOrderBy.Column, Is.EqualTo(orderBy.Column)); Assert.That(deserializedOrderBy.SortOrder, Is.EqualTo(orderBy.SortOrder)); } + + [Test] + public void B() + { + var t = BigInteger.Parse("238208892873504508097789456176502153024265176387") + * ( + BigInteger.Parse("31556952") + - BigInteger.Parse("4142484904995219") + ) + + BigInteger.Parse("238208892873504508097789456176502153024265176387") + * BigInteger.Parse("4142484904995219"); + + + } } \ No newline at end of file diff --git a/Circles.Index.Rpc/Circles.Index.Rpc.csproj b/Circles.Index.Rpc/Circles.Index.Rpc.csproj index 3d37fc0..b4070e2 100644 --- a/Circles.Index.Rpc/Circles.Index.Rpc.csproj +++ b/Circles.Index.Rpc/Circles.Index.Rpc.csproj @@ -15,6 +15,7 @@ + diff --git a/Circles.Index.Rpc/CirclesRpcModule.cs b/Circles.Index.Rpc/CirclesRpcModule.cs index 8a07dee..19b179d 100644 --- a/Circles.Index.Rpc/CirclesRpcModule.cs +++ b/Circles.Index.Rpc/CirclesRpcModule.cs @@ -3,6 +3,10 @@ using Circles.Index.Query; using Circles.Index.Query.Dto; using Circles.Index.Utils; +using Circles.Pathfinder; +using Circles.Pathfinder.Data; +using Circles.Pathfinder.DTOs; +using Circles.Pathfinder.Graphs; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -343,6 +347,14 @@ public ResultWrapper circles_events(Address? address, long? from eventTypes, filterPredicates, sortAscending)); } + public async Task> circlesV2_findPath(FlowRequest flowRequest) + { + var loadGraph = new LoadGraph(_indexerContext.Settings.IndexDbConnectionString); + var graphFactory = new GraphFactory(); + var pathfinder = new V2Pathfinder(loadGraph, graphFactory); + return ResultWrapper.Success(await pathfinder.ComputeMaxFlow(flowRequest)); + } + private string[] GetTokenExposureIds(Address address) { var selectTokenExposure = new Select( diff --git a/Circles.Index.Rpc/ICirclesRpcModule.cs b/Circles.Index.Rpc/ICirclesRpcModule.cs index 9c0c7c4..40ce281 100644 --- a/Circles.Index.Rpc/ICirclesRpcModule.cs +++ b/Circles.Index.Rpc/ICirclesRpcModule.cs @@ -1,5 +1,6 @@ using Circles.Index.Common; using Circles.Index.Query.Dto; +using Circles.Pathfinder.DTOs; using Nethermind.Core; using Nethermind.JsonRpc; using Nethermind.JsonRpc.Modules; @@ -59,4 +60,9 @@ public interface ICirclesRpcModule : IRpcModule IsImplemented = true)] ResultWrapper circles_events(Address? address, long? fromBlock, long? toBlock = null, string[]? eventTypes = null, FilterPredicateDto[]? filters = null, bool? sortAscending = false); + + [JsonRpcMethod( + Description = "Tries to find a transitive transfer path between two addresses in the Circles V2 graph", + IsImplemented = true)] + Task> circlesV2_findPath(FlowRequest flowRequest); } \ No newline at end of file diff --git a/Circles.Index/Circles.Index.csproj b/Circles.Index/Circles.Index.csproj index d80d958..99e285b 100644 --- a/Circles.Index/Circles.Index.csproj +++ b/Circles.Index/Circles.Index.csproj @@ -8,8 +8,8 @@ Daniel Janz (Gnosis Service GmbH) Gnosis Service GmbH Circles - 1.9.1 - 1.9.1 + 1.10.0 + 1.10.0 diff --git a/Circles.Pathfinder/Circles.Pathfinder.csproj b/Circles.Pathfinder/Circles.Pathfinder.csproj new file mode 100644 index 0000000..2a9213c --- /dev/null +++ b/Circles.Pathfinder/Circles.Pathfinder.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Circles.Pathfinder/DTOs/FlowRequest.cs b/Circles.Pathfinder/DTOs/FlowRequest.cs new file mode 100644 index 0000000..e1d90da --- /dev/null +++ b/Circles.Pathfinder/DTOs/FlowRequest.cs @@ -0,0 +1,8 @@ +namespace Circles.Pathfinder.DTOs; + +public class FlowRequest +{ + public string? Source { get; set; } + public string? Sink { get; set; } + public string? TargetFlow { get; set; } +} \ No newline at end of file diff --git a/Circles.Pathfinder/DTOs/MaxFlowResponse.cs b/Circles.Pathfinder/DTOs/MaxFlowResponse.cs new file mode 100644 index 0000000..10d080f --- /dev/null +++ b/Circles.Pathfinder/DTOs/MaxFlowResponse.cs @@ -0,0 +1,13 @@ +namespace Circles.Pathfinder.DTOs; + +public class MaxFlowResponse +{ + public string MaxFlow { get; set; } + public List Transfers { get; set; } + + public MaxFlowResponse(string maxFlow, List transfers) + { + MaxFlow = maxFlow; + Transfers = transfers; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/DTOs/TransferPathStep.cs b/Circles.Pathfinder/DTOs/TransferPathStep.cs new file mode 100644 index 0000000..3665f2a --- /dev/null +++ b/Circles.Pathfinder/DTOs/TransferPathStep.cs @@ -0,0 +1,9 @@ +namespace Circles.Pathfinder.DTOs; + +public class TransferPathStep +{ + public string From { get; set; } = string.Empty; + public string To { get; set; } = string.Empty; + public string TokenOwner { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; +} diff --git a/Circles.Pathfinder/Data/LoadGraph.cs b/Circles.Pathfinder/Data/LoadGraph.cs new file mode 100644 index 0000000..9b5afbe --- /dev/null +++ b/Circles.Pathfinder/Data/LoadGraph.cs @@ -0,0 +1,54 @@ +using Npgsql; + +namespace Circles.Pathfinder.Data +{ + // TODO: Use CirclesQuery and remove the Npgsql dependency + public class LoadGraph(string connectionString) + { + public IEnumerable<(string Balance, string Account, string TokenAddress)> LoadV2Balances() + { + var balanceQuery = @" + select ""demurragedTotalBalance""::text, ""account"", ""tokenAddress"" + from ""V_CrcV2_BalancesByAccountAndToken"" + where ""demurragedTotalBalance"" > 0; + "; + + using var connection = new NpgsqlConnection(connectionString); + connection.Open(); + + using var command = new NpgsqlCommand(balanceQuery, connection); + using var reader = command.ExecuteReader(); + + while (reader.Read()) + { + var balance = reader.GetString(0); + var account = reader.GetString(1); + var tokenAddress = reader.GetString(2); + + yield return (balance, account, tokenAddress); + } + } + + public IEnumerable<(string Truster, string Trustee, int Limit)> LoadV2Trust() + { + var trustQuery = @" + select truster, trustee + from ""V_CrcV2_TrustRelations""; + "; + + using var connection = new NpgsqlConnection(connectionString); + connection.Open(); + + using var command = new NpgsqlCommand(trustQuery, connection); + using var reader = command.ExecuteReader(); + + while (reader.Read()) + { + var truster = reader.GetString(0); + var trustee = reader.GetString(1); + + yield return (truster, trustee, 100); // Assuming a default trust limit of 100 in V2 + } + } + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Edges/CapacityEdge.cs b/Circles.Pathfinder/Edges/CapacityEdge.cs new file mode 100644 index 0000000..e3e423d --- /dev/null +++ b/Circles.Pathfinder/Edges/CapacityEdge.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Circles.Pathfinder.Edges; + +/// +/// Represents a capacity edge for potential token transfers between nodes. +/// +public class CapacityEdge : Edge +{ + public string Token { get; } + public BigInteger InitialCapacity { get; } + + public CapacityEdge(string from, string to, string token, BigInteger initialCapacity) : base(from, to) + { + Token = token; + InitialCapacity = initialCapacity; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Edges/Edge.cs b/Circles.Pathfinder/Edges/Edge.cs new file mode 100644 index 0000000..4145c46 --- /dev/null +++ b/Circles.Pathfinder/Edges/Edge.cs @@ -0,0 +1,13 @@ +namespace Circles.Pathfinder.Edges; + +public abstract class Edge +{ + public string From { get; } + public string To { get; } + + public Edge(string from, string to) + { + From = from; + To = to; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Edges/FlowEdge.cs b/Circles.Pathfinder/Edges/FlowEdge.cs new file mode 100644 index 0000000..5a40bd6 --- /dev/null +++ b/Circles.Pathfinder/Edges/FlowEdge.cs @@ -0,0 +1,20 @@ +using System.Numerics; + +namespace Circles.Pathfinder.Edges; + +/// +/// Represents a flow edge for actual token transfers between nodes. +/// +public class FlowEdge : CapacityEdge +{ + public BigInteger CurrentCapacity { get; set; } + public BigInteger Flow { get; set; } + public FlowEdge? ReverseEdge { get; set; } + + public FlowEdge(string from, string to, string token, BigInteger initialCapacity) + : base(from, to, token, initialCapacity) + { + CurrentCapacity = initialCapacity; + Flow = 0; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Edges/TrustEdge.cs b/Circles.Pathfinder/Edges/TrustEdge.cs new file mode 100644 index 0000000..0a1899c --- /dev/null +++ b/Circles.Pathfinder/Edges/TrustEdge.cs @@ -0,0 +1,11 @@ +namespace Circles.Pathfinder.Edges; + +/// +/// Represents a trust relationship between two nodes. +/// +public class TrustEdge : Edge +{ + public TrustEdge(string from, string to) : base(from, to) + { + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/BalanceGraph.cs b/Circles.Pathfinder/Graphs/BalanceGraph.cs new file mode 100644 index 0000000..9d89de2 --- /dev/null +++ b/Circles.Pathfinder/Graphs/BalanceGraph.cs @@ -0,0 +1,38 @@ +using System.Numerics; +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Nodes; + +namespace Circles.Pathfinder.Graphs; + +public class BalanceGraph : IGraph +{ + public IDictionary Nodes { get; } = new Dictionary(); + public HashSet Edges { get; } = new(); + public IDictionary BalanceNodes { get; } = new Dictionary(); + public IDictionary AvatarNodes { get; } = new Dictionary(); + + public void AddAvatar(string avatarAddress) + { + Nodes.Add(avatarAddress, new AvatarNode(avatarAddress)); + } + + public void AddBalance(string address, string token, BigInteger balance) + { + address = address.ToLower(); + token = token.ToLower(); + if (!AvatarNodes.ContainsKey(address)) + { + AvatarNodes.Add(address, new AvatarNode(address)); + } + + var balanceNode = new BalanceNode(address, token, balance); + Nodes.Add(balanceNode.Address, balanceNode); + BalanceNodes.Add(balanceNode.Address, balanceNode); + + var capacityEdge = new CapacityEdge(address, balanceNode.Address, token, balance); + Edges.Add(capacityEdge); + + AvatarNodes[address].OutEdges.Add(capacityEdge); + BalanceNodes[balanceNode.Address].InEdges.Add(capacityEdge); + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/CapacityGraph.cs b/Circles.Pathfinder/Graphs/CapacityGraph.cs new file mode 100644 index 0000000..eb4e6cc --- /dev/null +++ b/Circles.Pathfinder/Graphs/CapacityGraph.cs @@ -0,0 +1,63 @@ +using System.Numerics; +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Nodes; + +namespace Circles.Pathfinder.Graphs; + +public class CapacityGraph : IGraph +{ + public IDictionary Nodes { get; } = new Dictionary(); + public IDictionary AvatarNodes { get; } = new Dictionary(); + public IDictionary BalanceNodes { get; } = new Dictionary(); + public HashSet Edges { get; } = new(); + + public void AddAvatar(string avatarAddress) + { + avatarAddress = avatarAddress.ToLower(); + if (!AvatarNodes.ContainsKey(avatarAddress)) + { + AvatarNodes.Add(avatarAddress, new AvatarNode(avatarAddress)); + Nodes.Add(avatarAddress, AvatarNodes[avatarAddress]); + } + } + + public void AddBalanceNode(string address, string token, BigInteger amount) + { + address = address.ToLower(); + token = token.ToLower(); + + var balanceNode = new BalanceNode(address, token, amount); + balanceNode.Address = address; + BalanceNodes.TryAdd(balanceNode.Address, balanceNode); + Nodes.TryAdd(balanceNode.Address, balanceNode); + } + + public void AddCapacityEdge(string from, string to, string token, BigInteger capacity) + { + from = from.ToLower(); + to = to.ToLower(); + token = token.ToLower(); + + var edge = new CapacityEdge(from, to, token, capacity); + Edges.Add(edge); + + // Optionally, you can manage adjacency lists if needed + if (AvatarNodes.TryGetValue(from, out var node)) + { + node.OutEdges.Add(edge); + } + else if (BalanceNodes.TryGetValue(from, out var balanceNode)) + { + balanceNode.OutEdges.Add(edge); + } + + if (AvatarNodes.TryGetValue(to, out var avatarNode)) + { + avatarNode.InEdges.Add(edge); + } + else if (BalanceNodes.TryGetValue(to, out var balanceNode)) + { + balanceNode.InEdges.Add(edge); + } + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/FlowGraph.cs b/Circles.Pathfinder/Graphs/FlowGraph.cs new file mode 100644 index 0000000..5a44936 --- /dev/null +++ b/Circles.Pathfinder/Graphs/FlowGraph.cs @@ -0,0 +1,225 @@ +using System.Numerics; +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Nodes; + +namespace Circles.Pathfinder.Graphs; + +public class FlowGraph : IGraph +{ + public IDictionary Nodes { get; } = new Dictionary(); + public IDictionary AvatarNodes { get; } = new Dictionary(); + public IDictionary BalanceNodes { get; } = new Dictionary(); + public HashSet Edges { get; } = new(); + + public void AddAvatar(string avatarAddress) + { + if (!AvatarNodes.ContainsKey(avatarAddress)) + { + AvatarNodes.Add(avatarAddress, new AvatarNode(avatarAddress)); + Nodes.Add(avatarAddress, AvatarNodes[avatarAddress]); + } + } + + public void AddBalanceNode(string address, string token, BigInteger amount) + { + var balanceNode = new BalanceNode(address, token, amount); + balanceNode.Address = address; + BalanceNodes.TryAdd(balanceNode.Address, balanceNode); + Nodes.TryAdd(balanceNode.Address, balanceNode); + } + + public void AddCapacity(CapacityGraph capacityGraph) + { + foreach (var capacityEdge in capacityGraph.Edges) + { + AddCapacityEdge(capacityGraph, capacityEdge); + } + } + + public void AddFlowEdge(FlowGraph flowGraph, FlowEdge flowEdge) + { + var fromNode = flowGraph.Nodes[flowEdge.From]; + if (!Nodes.TryGetValue(fromNode.Address, out var from)) + { + if (fromNode is AvatarNode) + { + AddAvatar(fromNode.Address); + } + else if (fromNode is BalanceNode fromBalance) + { + AddBalanceNode(fromBalance.Address, fromBalance.Token, fromBalance.Amount); + } + + from = Nodes[fromNode.Address]; + } + + var toNode = flowGraph.Nodes[flowEdge.To]; + if (!Nodes.TryGetValue(toNode.Address, out var to)) + { + if (toNode is AvatarNode) + { + AddAvatar(toNode.Address); + } + else if (toNode is BalanceNode toBalance) + { + AddBalanceNode(toBalance.Address, toBalance.Token, toBalance.Amount); + } + + to = Nodes[toNode.Address]; + } + + if (from.OutEdges.Any(o => o.To == to.Address && (o as FlowEdge)?.Flow == flowEdge.Flow)) + { + return; + } + + if (to.InEdges.Any(o => o.From == from.Address && (o as FlowEdge)?.Flow == flowEdge.Flow)) + { + return; + } + + var newFlowEdge = new FlowEdge(from.Address, to.Address, flowEdge.Token, flowEdge.CurrentCapacity); + newFlowEdge.Flow = flowEdge.Flow; + + var newReverseEdge = new FlowEdge(to.Address, from.Address, flowEdge.Token, + flowEdge.ReverseEdge?.CurrentCapacity ?? BigInteger.Zero); + newReverseEdge.Flow = flowEdge.ReverseEdge?.Flow ?? BigInteger.Zero; + + newFlowEdge.ReverseEdge = newReverseEdge; + newReverseEdge.ReverseEdge = newFlowEdge; + + Edges.Add(newFlowEdge); + Edges.Add(newReverseEdge); + + if (AvatarNodes.TryGetValue(from.Address, out var fromAvatarNode)) + { + fromAvatarNode.OutEdges.Add(newFlowEdge); + } + else if (BalanceNodes.TryGetValue(from.Address, out var fromBalanceNode)) + { + fromBalanceNode.OutEdges.Add(newFlowEdge); + } + + if (AvatarNodes.TryGetValue(to.Address, out var toAvatarNode)) + { + toAvatarNode.InEdges.Add(newFlowEdge); + } + else if (BalanceNodes.TryGetValue(to.Address, out var toBalanceNode)) + { + toBalanceNode.InEdges.Add(newFlowEdge); + } + } + + public void AddCapacityEdge(CapacityGraph capacityGraph, CapacityEdge capacityEdge) + { + var from = capacityEdge.From; + var to = capacityEdge.To; + var token = capacityEdge.Token; + var capacity = capacityEdge.InitialCapacity; + + // Create edge and reverse edge + var edge = new FlowEdge(from, to, token, capacity); + Edges.Add(edge); + + var reverseEdge = new FlowEdge(to, from, token, 0); + Edges.Add(reverseEdge); + + edge.ReverseEdge = reverseEdge; + reverseEdge.ReverseEdge = edge; + + // Create nodes if they don't exist + var fromNode = capacityGraph.Nodes[from]; + if (fromNode is AvatarNode) + { + AddAvatar(fromNode.Address); + } + else if (fromNode is BalanceNode fromBalance) + { + AddBalanceNode(fromBalance.Address, fromBalance.Token, fromBalance.Amount); + } + + var toNode = capacityGraph.Nodes[to]; + if (toNode is AvatarNode) + { + AddAvatar(toNode.Address); + } + else if (toNode is BalanceNode toBalance) + { + AddBalanceNode(toBalance.Address, toBalance.Token, toBalance.Amount); + } + + // manage adjacency lists + if (AvatarNodes.TryGetValue(from, out var node)) + { + node.OutEdges.Add(edge); + } + else if (BalanceNodes.TryGetValue(from, out var balanceNode)) + { + balanceNode.OutEdges.Add(edge); + } + + if (AvatarNodes.TryGetValue(to, out var avatarNode)) + { + avatarNode.InEdges.Add(edge); + } + else if (BalanceNodes.TryGetValue(to, out var balanceNode)) + { + balanceNode.InEdges.Add(edge); + } + + if (AvatarNodes.TryGetValue(to, out var avatarNode2)) + { + avatarNode2.OutEdges.Add(reverseEdge); + } + else if (BalanceNodes.TryGetValue(to, out var balanceNode2)) + { + balanceNode2.OutEdges.Add(reverseEdge); + } + + if (AvatarNodes.TryGetValue(from, out var node2)) + { + node2.InEdges.Add(reverseEdge); + } + else if (BalanceNodes.TryGetValue(from, out var balanceNode2)) + { + balanceNode2.InEdges.Add(reverseEdge); + } + } + + public List> ExtractPathsWithFlow(string sourceNode, string sinkNode) + { + var resultPaths = new List>(); + var visited = new HashSet(); + + // A helper method to perform DFS and collect paths with positive flow + void Dfs(string currentNode, List currentPath) + { + if (currentNode == sinkNode) + { + resultPaths.Add(new List(currentPath)); // Store a copy of the path + return; + } + + if (!Nodes.TryGetValue(currentNode, out var node)) return; + + visited.Add(currentNode); + + foreach (var edge in node.OutEdges.OfType()) + { + if (edge.Flow > 0 && !visited.Contains(edge.To)) + { + currentPath.Add(edge); // Add edge to the current path + Dfs(edge.To, currentPath); // Recursively go deeper + currentPath.Remove(edge); // Backtrack + } + } + + visited.Remove(currentNode); + } + + // Start DFS from the source node + Dfs(sourceNode, new List()); + + return resultPaths; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/GraphExtensions.cs b/Circles.Pathfinder/Graphs/GraphExtensions.cs new file mode 100644 index 0000000..a4bc9a5 --- /dev/null +++ b/Circles.Pathfinder/Graphs/GraphExtensions.cs @@ -0,0 +1,119 @@ +using System.Numerics; +using Circles.Pathfinder.Edges; + +namespace Circles.Pathfinder.Graphs; + +public static class GraphExtensions +{ + /// + /// Computes the maximum flow from source to sink in the FlowGraph and collects all augmenting paths. + /// Stops when the target flow is met and prunes the flow to exactly match the target. + /// + /// The flow graph instance. + /// The source node identifier. + /// The sink node identifier. + /// The desired flow value to reach. + /// The total flow value up to the target flow. + public static BigInteger ComputeMaxFlowWithPaths( + this FlowGraph graph, + string source, + string sink, + BigInteger targetFlow) + { + BigInteger maxFlow = 0; + + while (true) + { + // Use BFS to find an augmenting path + var (path, pathFlow) = graph.Bfs(source, sink); + + if (pathFlow == 0) + { + break; // No more augmenting paths + } + + // Calculate how much more flow we need + BigInteger remainingFlow = targetFlow - maxFlow; + + // If the pathFlow exceeds the remaining targetFlow, prune it to the remaining amount + if (pathFlow > remainingFlow) + { + pathFlow = remainingFlow; + } + + // Update the flow and capacities along the path + foreach (var edge in path) + { + edge.Flow += pathFlow; + edge.CurrentCapacity -= pathFlow; + + if (edge.ReverseEdge != null) + { + edge.ReverseEdge.CurrentCapacity += pathFlow; + } + } + + // Update the accumulated maxFlow + maxFlow += pathFlow; + + // Stop if we've reached the target flow exactly + if (maxFlow >= targetFlow) + { + break; + } + } + + // Return the accumulated flow, limited to the targetFlow + return maxFlow; + } + + /// + /// Finds the shortest augmenting path in the FlowGraph using BFS and calculates the flow that can be pushed through it. + /// + /// The flow graph instance. + /// The source node identifier. + /// The sink node identifier. + /// A tuple containing the list of edges constituting the path and the flow that can be pushed through this path. + private static (List path, BigInteger flow) Bfs(this FlowGraph graph, string source, string sink) + { + var visited = new HashSet(); + var queue = new Queue<(string node, List path)>(); + visited.Add(source); + queue.Enqueue((source, new List())); + + while (queue.Count > 0) + { + var (currentNode, currentPath) = queue.Dequeue(); + + // Check if the current node has outgoing edges + if (!graph.Nodes.TryGetValue(currentNode, out var node)) + { + continue; + } + + // Iterate through all outgoing edges of the current node + foreach (var edge in node.OutEdges.OfType()) + { + if (edge.CurrentCapacity > 0 && !visited.Contains(edge.To)) + { + // Append the current edge to the path + var newPath = new List(currentPath) { edge }; + + // If the sink is reached, return the path immediately + if (edge.To == sink) + { + BigInteger pathFlow = newPath.Min(e => e.CurrentCapacity); + return (newPath, pathFlow); + } + + // Otherwise, continue traversing + visited.Add(edge.To); + queue.Enqueue((edge.To, newPath)); + } + } + } + + // No valid path found + return (new List(), 0); + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/GraphFactory.cs b/Circles.Pathfinder/Graphs/GraphFactory.cs new file mode 100644 index 0000000..1b9fafa --- /dev/null +++ b/Circles.Pathfinder/Graphs/GraphFactory.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using Circles.Pathfinder.Data; + +namespace Circles.Pathfinder.Graphs +{ + public class GraphFactory + { + /// + /// Loads all v2 trust edges from the database and creates a trust graph from them. + /// + /// A trust graph containing all v2 trust edges. + public TrustGraph V2TrustGraph(LoadGraph loadGraph) + { + var trustEdges = loadGraph.LoadV2Trust().ToArray(); + var graph = new TrustGraph(); + foreach (var trustEdge in trustEdges) + { + if (!graph.AvatarNodes.ContainsKey(trustEdge.Truster)) + { + graph.AddAvatar(trustEdge.Truster); + } + + if (!graph.AvatarNodes.ContainsKey(trustEdge.Trustee)) + { + graph.AddAvatar(trustEdge.Trustee); + } + + graph.AddTrustEdge(trustEdge.Truster, trustEdge.Trustee); + } + + return graph; + } + + /// + /// Loads all v2 balances from the database and creates a balance graph from them. + /// + /// A balance graph containing all v2 balances and holders. + public BalanceGraph V2BalanceGraph(LoadGraph loadGraph) + { + var balances = loadGraph.LoadV2Balances().ToArray(); + var graph = new BalanceGraph(); + foreach (var balance in balances) + { + if (!graph.AvatarNodes.ContainsKey(balance.Account)) + { + graph.AddAvatar(balance.Account); + } + + graph.AddBalance(balance.Account, balance.TokenAddress, BigInteger.Parse(balance.Balance)); + } + + return graph; + } + + /// + /// Takes a balance graph and a trust graph and creates a capacity graph from them. + /// + /// The balance graph to use. + /// The trust graph to use. + /// A capacity graph created from the balance and trust graphs. + public CapacityGraph CreateCapacityGraph(BalanceGraph balanceGraph, TrustGraph trustGraph) + { + // Take the balance and trust graphs and create a capacity graph. + // 1. Create a unified list of nodes from both graphs + // 2. Leave the capacity edges from the balance graph in place + // 3. Create more capacity edges based on the trust graph: + // - For each balance, check if there is a node that is willing to accept the balance (is trusting the token issuer) + // - If there is, create a capacity edge from the balance node to the accepting node + + var capacityGraph = new CapacityGraph(); + + // Step 1: Create a unified list of nodes from both graphs + foreach (var avatar in balanceGraph.AvatarNodes.Values) + { + capacityGraph.AddAvatar(avatar.Address); + } + + foreach (var avatar in trustGraph.AvatarNodes.Values) + { + capacityGraph.AddAvatar(avatar.Address); + } + + // Add BalanceNodes + foreach (var balanceNode in balanceGraph.BalanceNodes.Values) + { + capacityGraph.AddBalanceNode(balanceNode.Address, balanceNode.Token, balanceNode.Amount); + } + + // Step 2: Leave the capacity edges from the balance graph in place + foreach (var capacityEdge in balanceGraph.Edges) + { + capacityGraph.AddCapacityEdge( + capacityEdge.From, + capacityEdge.To, + capacityEdge.Token, + capacityEdge.InitialCapacity + ); + } + + // Step 3: Create more capacity edges based on the trust graph + // Optimization: Precompute a trustee-to-trusters lookup dictionary + var trusteeToTrusters = new Dictionary>(); + + foreach (var edge in trustGraph.Edges) + { + if (!trusteeToTrusters.TryGetValue(edge.To, out var trusters)) + { + trusters = new List(); + trusteeToTrusters[edge.To] = trusters; + } + + trusters.Add(edge.From); + } + + foreach (var balanceNode in balanceGraph.BalanceNodes.Values) + { + string tokenIssuer = balanceNode.Token; + + if (trusteeToTrusters.TryGetValue(tokenIssuer, out var acceptingNodes)) + { + foreach (var acceptingNode in acceptingNodes) + { + // Avoid creating edges to self or invalid nodes + if (acceptingNode == balanceNode.HolderAddress) + continue; + + capacityGraph.AddCapacityEdge( + balanceNode.Address, + acceptingNode, + balanceNode.Token, + balanceNode.Amount + ); + } + } + } + + return capacityGraph; + } + + public FlowGraph CreateFlowGraph(CapacityGraph capacityGraph) + { + var flowGraph = new FlowGraph(); + flowGraph.AddCapacity(capacityGraph); + + return flowGraph; + } + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/GraphTraversal.cs b/Circles.Pathfinder/Graphs/GraphTraversal.cs new file mode 100644 index 0000000..a7bab9f --- /dev/null +++ b/Circles.Pathfinder/Graphs/GraphTraversal.cs @@ -0,0 +1,138 @@ +using Circles.Pathfinder.Edges; + +namespace Circles.Pathfinder.Graphs; + +/// +/// Enumeration for specifying graph traversal algorithms. +/// +public enum TraversalType +{ + DFS, + BFS +} + +/// +/// Provides graph traversal methods using DFS and BFS. +/// +public static class GraphTraversal +{ + /// + /// Traverses the graph from the start node to the end node using the specified traversal algorithm. + /// Optionally filters edges based on a predicate and allows early termination after the first path. + /// + /// The type of edge in the graph, must inherit from Edge. + /// The graph instance. + /// The starting node identifier. + /// The ending node identifier. + /// The traversal algorithm to use (DFS or BFS). + /// A predicate to filter edges. If null, all edges are considered. + /// If true, the traversal stops after finding the first path. + /// An enumerable of paths, where each path is a list of edges. + public static IEnumerable> Traverse( + this IGraph graph, + string start, + string end, + TraversalType traversalType, + Func? edgePredicate = null, + bool returnAfterFirst = false) + where TEdge : Edge + { + edgePredicate ??= _ => true; // If no predicate is provided, consider all edges. + + switch (traversalType) + { + case TraversalType.DFS: + return DfsTraverse(graph, start, end, edgePredicate, returnAfterFirst); + case TraversalType.BFS: + return BfsTraverse(graph, start, end, edgePredicate, returnAfterFirst); + default: + throw new ArgumentException("Invalid traversal type."); + } + } + + /// + /// Performs an iterative DFS traversal. + /// + private static IEnumerable> DfsTraverse( + IGraph graph, + string start, + string end, + Func? edgePredicate, + bool returnAfterFirst) + where TEdge : Edge + { + var stack = new Stack<(string currentNode, List path, HashSet visited)>(); + stack.Push((start, new List(), new HashSet { start })); + + while (stack.Count > 0) + { + var (current, path, visited) = stack.Pop(); + + if (current == end) + { + yield return path; + + if (returnAfterFirst) + yield break; + + continue; + } + + if (graph.Nodes.TryGetValue(current, out var node)) + { + foreach (var edge in node.OutEdges.OfType().Where(edgePredicate).Reverse()) + { + if (!visited.Contains(edge.To)) + { + var newPath = new List(path) { edge }; + var newVisited = new HashSet(visited) { edge.To }; + stack.Push((edge.To, newPath, newVisited)); + } + } + } + } + } + + /// + /// Performs an iterative BFS traversal. + /// + private static IEnumerable> BfsTraverse( + IGraph graph, + string start, + string end, + Func? edgePredicate, + bool returnAfterFirst) + where TEdge : Edge + { + var queue = new Queue<(string currentNode, List path, HashSet visited)>(); + queue.Enqueue((start, new List(), new HashSet { start })); + + while (queue.Count > 0) + { + var (current, path, visited) = queue.Dequeue(); + + if (current == end) + { + yield return path; + + if (returnAfterFirst) + yield break; + + continue; + } + + if (graph.Nodes.TryGetValue(current, out var node)) + { + foreach (var edge in node.OutEdges.OfType().Where(edgePredicate)) + { + if (!visited.Contains(edge.To)) + { + var newPath = new List(path) { edge }; + var newVisited = new HashSet(visited) { edge.To }; + queue.Enqueue((edge.To, newPath, newVisited)); + } + } + } + } + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/IGraph.cs b/Circles.Pathfinder/Graphs/IGraph.cs new file mode 100644 index 0000000..2d97a29 --- /dev/null +++ b/Circles.Pathfinder/Graphs/IGraph.cs @@ -0,0 +1,11 @@ +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Nodes; + +namespace Circles.Pathfinder.Graphs; + +public interface IGraph + where TEdge : Edge +{ + IDictionary Nodes { get; } + HashSet Edges { get; } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Graphs/TrustGraph.cs b/Circles.Pathfinder/Graphs/TrustGraph.cs new file mode 100644 index 0000000..1ef8c82 --- /dev/null +++ b/Circles.Pathfinder/Graphs/TrustGraph.cs @@ -0,0 +1,41 @@ +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Nodes; + +namespace Circles.Pathfinder.Graphs; + +public class TrustGraph : IGraph +{ + public IDictionary Nodes { get; } = new Dictionary(); + public IDictionary AvatarNodes { get; } = new Dictionary(); + public HashSet Edges { get; } = new(); + + public void AddAvatar(string avatarAddress) + { + var avatar = new AvatarNode(avatarAddress); + AvatarNodes.Add(avatarAddress, avatar); + Nodes.Add(avatarAddress, avatar); + } + + public void AddTrustEdge(string truster, string trustee) + { + truster = truster.ToLower(); + trustee = trustee.ToLower(); + + if (!AvatarNodes.TryGetValue(truster, out var trusterNode)) + { + trusterNode = new AvatarNode(truster); + AvatarNodes[truster] = trusterNode; + } + + if (!AvatarNodes.TryGetValue(trustee, out var trusteeNode)) + { + trusteeNode = new AvatarNode(trustee); + AvatarNodes[trustee] = trusteeNode; + } + + trusterNode.OutEdges.Add(new TrustEdge(truster, trustee)); + AvatarNodes[trustee].InEdges.Add(new TrustEdge(truster, trustee)); + + Edges.Add(new TrustEdge(truster, trustee)); + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/IPathfinder.cs b/Circles.Pathfinder/IPathfinder.cs new file mode 100644 index 0000000..f9358a5 --- /dev/null +++ b/Circles.Pathfinder/IPathfinder.cs @@ -0,0 +1,8 @@ +using Circles.Pathfinder.DTOs; + +namespace Circles.Pathfinder; + +public interface IPathfinder +{ + public Task ComputeMaxFlow(FlowRequest request); +} \ No newline at end of file diff --git a/Circles.Pathfinder/Nodes/AvatarNode.cs b/Circles.Pathfinder/Nodes/AvatarNode.cs new file mode 100644 index 0000000..5abb068 --- /dev/null +++ b/Circles.Pathfinder/Nodes/AvatarNode.cs @@ -0,0 +1,8 @@ +namespace Circles.Pathfinder.Nodes; + +public class AvatarNode : Node +{ + public AvatarNode(string address) : base(address) + { + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Nodes/BalanceNode.cs b/Circles.Pathfinder/Nodes/BalanceNode.cs new file mode 100644 index 0000000..1adcad5 --- /dev/null +++ b/Circles.Pathfinder/Nodes/BalanceNode.cs @@ -0,0 +1,17 @@ +using System.Numerics; + +namespace Circles.Pathfinder.Nodes; + +public class BalanceNode : Node +{ + public string Token { get; } + public BigInteger Amount { get; } + + public string HolderAddress => Address.Split("-")[0]; + + public BalanceNode(string address, string token, BigInteger amount) : base(address + "-" + token) + { + Token = token; + Amount = amount; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/Nodes/Node.cs b/Circles.Pathfinder/Nodes/Node.cs new file mode 100644 index 0000000..7708737 --- /dev/null +++ b/Circles.Pathfinder/Nodes/Node.cs @@ -0,0 +1,15 @@ +using Circles.Pathfinder.Edges; + +namespace Circles.Pathfinder.Nodes; + +public abstract class Node +{ + public string Address { get; set; } + public List OutEdges { get; } = new(); + public List InEdges { get; } = new(); + + protected Node(string address) + { + Address = address; + } +} \ No newline at end of file diff --git a/Circles.Pathfinder/V2Pathfinder.cs b/Circles.Pathfinder/V2Pathfinder.cs new file mode 100644 index 0000000..5e241a6 --- /dev/null +++ b/Circles.Pathfinder/V2Pathfinder.cs @@ -0,0 +1,209 @@ +using System.Numerics; +using Circles.Pathfinder.Data; +using Circles.Pathfinder.DTOs; +using Circles.Pathfinder.Edges; +using Circles.Pathfinder.Graphs; + +namespace Circles.Pathfinder; + +public class V2Pathfinder : IPathfinder +{ + private readonly LoadGraph _loadGraph; + private readonly GraphFactory _graphFactory; + + public V2Pathfinder(LoadGraph loadGraph, GraphFactory graphFactory) + { + _loadGraph = loadGraph; + _graphFactory = graphFactory; + } + + public async Task ComputeMaxFlow(FlowRequest request) + { + if (string.IsNullOrEmpty(request.Source) || string.IsNullOrEmpty(request.Sink)) + { + throw new ArgumentException("Source and Sink must be provided."); + } + + if (!BigInteger.TryParse(request.TargetFlow, out var targetFlow)) + { + throw new ArgumentException("TargetFlow must be a valid integer."); + } + + // Load Trust and Balance Graphs + var trustGraph = _graphFactory.V2TrustGraph(_loadGraph); + var balanceGraph = _graphFactory.V2BalanceGraph(_loadGraph); + + // Create Capacity Graph + var capacityGraph = _graphFactory.CreateCapacityGraph(balanceGraph, trustGraph); + + // Create Flow Graph + var flowGraph = _graphFactory.CreateFlowGraph(capacityGraph); + + // Validate Source and Sink + if (!flowGraph.Nodes.ContainsKey(request.Source)) + { + throw new ArgumentException($"Source node '{request.Source}' does not exist in the graph."); + } + + if (!flowGraph.Nodes.ContainsKey(request.Sink)) + { + throw new ArgumentException($"Sink node '{request.Sink}' does not exist in the graph."); + } + + if (IsBalanceNode(request.Source)) + { + throw new ArgumentException("Source node cannot be a balance node."); + } + + if (IsBalanceNode(request.Sink)) + { + throw new ArgumentException("Sink node cannot be a balance node."); + } + + // Compute Max Flow + var maxFlow = flowGraph.ComputeMaxFlowWithPaths(request.Source, request.Sink, targetFlow); + + // Extract Paths with Flow + var pathsWithFlow = flowGraph.ExtractPathsWithFlow(request.Source, request.Sink); + + // Collapse balance nodes to get a collapsed graph + var collapsedGraph = CollapseBalanceNodes(pathsWithFlow); + + // Create transfer steps from the collapsed graph + var transferSteps = new List(); + + foreach (var edge in collapsedGraph.Edges) + { + // For each edge, create a transfer step + if (edge.Flow == BigInteger.Zero) + { + // Filter reverse edges + continue; + } + + transferSteps.Add(new TransferPathStep + { + From = edge.From, + To = edge.To, + TokenOwner = edge.Token, + Value = edge.Flow.ToString() + }); + } + + // Prepare the response + var response = new MaxFlowResponse(maxFlow.ToString(), transferSteps); + + return response; + } + + /// + /// Collapses balance nodes in the paths and returns a collapsed flow graph. + /// + /// The list of paths with flow. + /// A FlowGraph with balance nodes collapsed. + private FlowGraph CollapseBalanceNodes(List> pathsWithFlow) + { + var collapsedGraph = new FlowGraph(); + + // 1. Collect all avatar nodes + var avatars = new HashSet(); + pathsWithFlow.ForEach(o => o.ForEach(p => + { + if (!IsBalanceNode(p.From)) + { + avatars.Add(p.From); + } + + if (!IsBalanceNode(p.To)) + { + avatars.Add(p.To); + } + })); + foreach (var avatar in avatars) + { + collapsedGraph.AddAvatar(avatar); + } + + // 2. Remove all balance nodes, fuse the ends together, and add that edge to the new flow graph + pathsWithFlow.ForEach(o => + { + for (int i = 0; i < o.Count; i++) + { + var currentEdge = o[i]; + var nextEdge = i < o.Count - 1 ? o[i + 1] : null; + + if (IsBalanceNode(currentEdge.To) && nextEdge != null && nextEdge.From == currentEdge.To) + { + // We are at a balance node, so we need to collapse it by merging currentEdge and nextEdge + + // The flow through the balance node is limited by both the incoming and outgoing flows + var mergedFlow = BigInteger.Min(currentEdge.Flow, nextEdge.Flow); + + var mergedEdge = new FlowEdge( + currentEdge.From, + nextEdge.To, + nextEdge.Token, + currentEdge.CurrentCapacity // Adjust as needed + ) + { + Flow = mergedFlow + }; + try + { + collapsedGraph.AddFlowEdge(collapsedGraph, mergedEdge); + i++; // Skip the nextEdge since we have merged it + } + catch (Exception e) + { + Console.WriteLine(e.Message); + + // Log the stack trace + Console.WriteLine(e.StackTrace); + + // Unpack the inner exception(s) recursively + while (e.InnerException != null) + { + e = e.InnerException; + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + } + } + } + else + { + try + { + // If not a balance node, add the current edge to the collapsed graph + collapsedGraph.AddFlowEdge(collapsedGraph, currentEdge); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + + // Log the stack trace + Console.WriteLine(e.StackTrace); + + // Unpack the inner exception(s) recursively + while (e.InnerException != null) + { + e = e.InnerException; + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + } + } + } + } + }); + return collapsedGraph; + } + + /// + /// Determines if a given node address is a balance node. + /// + /// The node address to check. + /// True if it's a balance node; otherwise, false. + private bool IsBalanceNode(string nodeAddress) + { + return nodeAddress.Contains("-"); + } +} \ No newline at end of file diff --git a/Circles.sln b/Circles.sln index ce6f8e1..6b8397a 100644 --- a/Circles.sln +++ b/Circles.sln @@ -69,6 +69,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "protobuf-types", "protobuf- v2.standardTreasury.proto = v2.standardTreasury.proto EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Circles.Pathfinder", "Circles.Pathfinder\Circles.Pathfinder.csproj", "{756580B1-2D2A-4FFE-9711-046A10853003}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +125,10 @@ Global {C68CD7EC-9F4D-481E-AC4E-E063794E9488}.Debug|Any CPU.Build.0 = Debug|Any CPU {C68CD7EC-9F4D-481E-AC4E-E063794E9488}.Release|Any CPU.ActiveCfg = Release|Any CPU {C68CD7EC-9F4D-481E-AC4E-E063794E9488}.Release|Any CPU.Build.0 = Release|Any CPU + {756580B1-2D2A-4FFE-9711-046A10853003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {756580B1-2D2A-4FFE-9711-046A10853003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {756580B1-2D2A-4FFE-9711-046A10853003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {756580B1-2D2A-4FFE-9711-046A10853003}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2B7D2126-6D6F-4A5B-81A1-3E5F1A9645F6} = {64189094-62E5-48CA-BD66-1A7B82263BA8} diff --git a/arm64.Dockerfile b/arm64.Dockerfile index 9bee603..115e014 100644 --- a/arm64.Dockerfile +++ b/arm64.Dockerfile @@ -20,5 +20,6 @@ COPY --from=build /circles-nethermind-plugin/Circles.Index.CirclesViews.dll /net COPY --from=build /circles-nethermind-plugin/Circles.Index.Rpc.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.dll /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Nethermind.Int256.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Npgsql.dll /nethermind/plugins diff --git a/x64.Dockerfile b/x64.Dockerfile index 8c6b56b..11c24db 100644 --- a/x64.Dockerfile +++ b/x64.Dockerfile @@ -21,5 +21,6 @@ COPY --from=build /circles-nethermind-plugin/Circles.Index.Postgres.dll /netherm COPY --from=build /circles-nethermind-plugin/Circles.Index.Rpc.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.dll /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Nethermind.Int256.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Npgsql.dll /nethermind/plugins diff --git a/x64.debug.Dockerfile b/x64.debug.Dockerfile index 90f744c..c6ee674 100644 --- a/x64.debug.Dockerfile +++ b/x64.debug.Dockerfile @@ -32,5 +32,7 @@ COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.dll /nethermind COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.pdb /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.pdb /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.dll /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.pdb /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Nethermind.Int256.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Npgsql.dll /nethermind/plugins \ No newline at end of file diff --git a/x64.debug.spaceneth.Dockerfile b/x64.debug.spaceneth.Dockerfile index 752a0f7..9c18c0f 100644 --- a/x64.debug.spaceneth.Dockerfile +++ b/x64.debug.spaceneth.Dockerfile @@ -32,6 +32,8 @@ COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.dll /nethermind COPY --from=build /circles-nethermind-plugin/Circles.Index.Query.pdb /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Circles.Index.Utils.pdb /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.dll /nethermind/plugins +COPY --from=build /circles-nethermind-plugin/Circles.Pathfinder.pdb /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Nethermind.Int256.dll /nethermind/plugins COPY --from=build /circles-nethermind-plugin/Npgsql.dll /nethermind/plugins From 855aedcef2622b1804bf645996a544353417dda7 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:14:16 +0200 Subject: [PATCH 2/5] remove the spaceneth section from the readme --- Readme.md | 135 +----------------------------------------------------- 1 file changed, 2 insertions(+), 133 deletions(-) diff --git a/Readme.md b/Readme.md index beb782b..0643b52 100644 --- a/Readme.md +++ b/Readme.md @@ -12,12 +12,6 @@ query [Circles](https://www.aboutcircles.com/) protocol events. * [Run node](#4-run-node) * [Ports](#ports) * [Volumes](#volumes) - * [Run a spaceneth node](#run-a-spaceneth-node) - * [Deploying the Circles contracts](#deploying-the-circles-contracts) - * [Blockscout](#blockscout) - * [Get a funded account](#get-a-funded-account) - * [Manipulate time](#manipulate-time) - * [Reset the spaceneth node](#reset-the-spaceneth-node) * [Circles RPC methods](#circles-rpc-methods) * [circles_getTotalBalance / circlesV2_getTotalBalance](#circles_gettotalbalance--circlesv2_gettotalbalance) * [circles_getTokenBalances](#circles_gettokenbalances) @@ -51,10 +45,10 @@ For a detailed description of the available RPC methods, see the [Circles RPC me ### Run a node The repository contains a docker-compose file to start a Nethermind node with the Circles plugin installed. There are -configurations for Gnosis Chain, Chiado and Spaceneth (a local testnet). +configurations for Gnosis Chain and Chiado. The quickstart configurations use [lighthouse](https://github.com/sigp/lighthouse) as consensus engine and spin up a -postgres database to store the indexed data. The spaceneth configuration comes with a local blockscout instance. +postgres database to store the indexed data. #### 1. Clone the repository @@ -124,131 +118,6 @@ at the same RPC endpoint. * `./.state/postgres-chiado|postgres-gnosis` - Postgres data * `./.state/jwtsecret-chiado|jwtsecret-gnosis` - Shared secret between execution and consensus engine -### Run a spaceneth node - -The process of setting up a local only node is a bit more involved. However, by using this approach, you gain -the possibility to manipulate the node's time and don't need any xDai, which is useful for testing purposes. - -```bash -docker compose -f docker-compose.spaceneth.yml up -d -``` - -#### Deploying the Circles contracts - -Since a new spaceneth node is empty except for the genesis block, you need to deploy the Circles contracts yourself. - -```bash -# Clone the Circles contracts submodules -git submodule update --init --recursive -``` - -To deploy the contracts, we add a script to the Circles contracts repository that deploys the contracts to the spaceneth -node. - -```bash -# Add the deploy script to the Circles contracts repository -./add-deploy-script-tp-v2-repo.sh -``` - -As a last step, we need to replace the `tload` and `tstore` based reentrancy guards with a more classic approach. -Spaceneth does not support these instructions. - -1. Open `circles-contracts-v2/src/hub/Hub.sol` -2. Replace this modifier: - ```solidity - modifier nonReentrant(uint8 _code) { - assembly { - if tload(0) { revert(0, 0) } - tstore(0, 1) - } - _; - assembly { - tstore(0, 0) - } - } - ``` - - with this modifier: - - ```solidity - bool private _reentrancyGuard; - modifier nonReentrant(uint8 _code) { - if (_reentrancyGuard) { - revert CirclesReentrancyGuard(_code); - } - _reentrancyGuard = true; - _; - _reentrancyGuard = false; - } - ``` -3. Open `circles-contracts-v2/foundry.toml` -4. Remove this line: - ```toml - evm_version = 'cancun' - ``` - -Now you can deploy the contracts to the spaceneth node. - -```bash -# Deploy the contracts -npm install && ./deploy.sh -``` - -#### Blockscout - -You can access the blockscout instance at `http://localhost:4000`. - -#### Get a funded account - -You can get a funded account private key by running: - -```bash -npm install -node createFundedAccount.js -``` - -#### Manipulate time - -You can fast-forward the time by running: - -```bash -curl -X POST -H "Content-Type: application/json" -d '{"fake_time": "+1d x1"}' http://localhost:5000/set_time -``` - -**Explanation:** - -```json -{ - "fake_time": "+1d x1" -} -``` - -`+1d` means to offset the current time by 1 day. `x1` means that the time will pass as real time. If you want to -fast-forward the time, you can increase the number of `x` (e.g. `x10`). - -_NOTE: This will restart the nethermind node._ - -#### Reset the spaceneth node - -If you want to start over, you can reset the spaceneth node by running: - -```bash -# Stop the stack -docker compose -f docker-compose.spaceneth.yml down -``` - -```bash -# Delete all persisted data -sudo rm -rf .state/nethermind-spaceneth -sudo rm -rf .state/postgres-spaceneth -sudo rm -rf .state/postgres2-spaceneth -sudo rm -rf .state/redis-spaceneth -``` - -```bash -# Start the stack again -docker compose -f docker-compose.spaceneth.yml up -``` ## Circles RPC methods From 73fb36e92f73386cb55b16ec4c45ef39b2e2ba0e Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:17:53 +0200 Subject: [PATCH 3/5] removed unused class --- Circles.Pathfinder/Graphs/GraphTraversal.cs | 138 -------------------- Circles.sln | 10 -- 2 files changed, 148 deletions(-) delete mode 100644 Circles.Pathfinder/Graphs/GraphTraversal.cs diff --git a/Circles.Pathfinder/Graphs/GraphTraversal.cs b/Circles.Pathfinder/Graphs/GraphTraversal.cs deleted file mode 100644 index a7bab9f..0000000 --- a/Circles.Pathfinder/Graphs/GraphTraversal.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Circles.Pathfinder.Edges; - -namespace Circles.Pathfinder.Graphs; - -/// -/// Enumeration for specifying graph traversal algorithms. -/// -public enum TraversalType -{ - DFS, - BFS -} - -/// -/// Provides graph traversal methods using DFS and BFS. -/// -public static class GraphTraversal -{ - /// - /// Traverses the graph from the start node to the end node using the specified traversal algorithm. - /// Optionally filters edges based on a predicate and allows early termination after the first path. - /// - /// The type of edge in the graph, must inherit from Edge. - /// The graph instance. - /// The starting node identifier. - /// The ending node identifier. - /// The traversal algorithm to use (DFS or BFS). - /// A predicate to filter edges. If null, all edges are considered. - /// If true, the traversal stops after finding the first path. - /// An enumerable of paths, where each path is a list of edges. - public static IEnumerable> Traverse( - this IGraph graph, - string start, - string end, - TraversalType traversalType, - Func? edgePredicate = null, - bool returnAfterFirst = false) - where TEdge : Edge - { - edgePredicate ??= _ => true; // If no predicate is provided, consider all edges. - - switch (traversalType) - { - case TraversalType.DFS: - return DfsTraverse(graph, start, end, edgePredicate, returnAfterFirst); - case TraversalType.BFS: - return BfsTraverse(graph, start, end, edgePredicate, returnAfterFirst); - default: - throw new ArgumentException("Invalid traversal type."); - } - } - - /// - /// Performs an iterative DFS traversal. - /// - private static IEnumerable> DfsTraverse( - IGraph graph, - string start, - string end, - Func? edgePredicate, - bool returnAfterFirst) - where TEdge : Edge - { - var stack = new Stack<(string currentNode, List path, HashSet visited)>(); - stack.Push((start, new List(), new HashSet { start })); - - while (stack.Count > 0) - { - var (current, path, visited) = stack.Pop(); - - if (current == end) - { - yield return path; - - if (returnAfterFirst) - yield break; - - continue; - } - - if (graph.Nodes.TryGetValue(current, out var node)) - { - foreach (var edge in node.OutEdges.OfType().Where(edgePredicate).Reverse()) - { - if (!visited.Contains(edge.To)) - { - var newPath = new List(path) { edge }; - var newVisited = new HashSet(visited) { edge.To }; - stack.Push((edge.To, newPath, newVisited)); - } - } - } - } - } - - /// - /// Performs an iterative BFS traversal. - /// - private static IEnumerable> BfsTraverse( - IGraph graph, - string start, - string end, - Func? edgePredicate, - bool returnAfterFirst) - where TEdge : Edge - { - var queue = new Queue<(string currentNode, List path, HashSet visited)>(); - queue.Enqueue((start, new List(), new HashSet { start })); - - while (queue.Count > 0) - { - var (current, path, visited) = queue.Dequeue(); - - if (current == end) - { - yield return path; - - if (returnAfterFirst) - yield break; - - continue; - } - - if (graph.Nodes.TryGetValue(current, out var node)) - { - foreach (var edge in node.OutEdges.OfType().Where(edgePredicate)) - { - if (!visited.Contains(edge.To)) - { - var newPath = new List(path) { edge }; - var newVisited = new HashSet(visited) { edge.To }; - queue.Enqueue((edge.To, newPath, newVisited)); - } - } - } - } - } -} \ No newline at end of file diff --git a/Circles.sln b/Circles.sln index 6b8397a..4fe7154 100644 --- a/Circles.sln +++ b/Circles.sln @@ -20,23 +20,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{B30CB7 x64.Dockerfile = x64.Dockerfile arm64.Dockerfile = arm64.Dockerfile docker-compose.chiado.yml = docker-compose.chiado.yml - docker-compose.spaceneth.yml = docker-compose.spaceneth.yml .dockerignore = .dockerignore - circles-chainspec.json = circles-chainspec.json - circles-config.cfg = circles-config.cfg - time_controller.py = time_controller.py x64.debug.Dockerfile = x64.debug.Dockerfile .env.example = .env.example docker-compose.gnosis.yml = docker-compose.gnosis.yml Readme.md = Readme.md - envs\common-smart-contract-verifier.env = envs\common-smart-contract-verifier.env - envs\common-blockscout.env = envs\common-blockscout.env .gitmodules = .gitmodules - createFundedAccount.js = createFundedAccount.js - package.json = package.json - deploy.sh = deploy.sh .gitignore = .gitignore - add-deploy-script-tp-v2-repo.sh = add-deploy-script-tp-v2-repo.sh v2-example-requests.md = v2-example-requests.md v1-example-requests.md = v1-example-requests.md general-example-requests.md = general-example-requests.md From d8614bc7bd68b8b7239468f8dff053e0d7302a67 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 24 Oct 2024 03:29:21 +0200 Subject: [PATCH 4/5] Filter paths with less than 0.1 Circles --- Circles.Pathfinder/Graphs/FlowGraph.cs | 15 ++++++++++++++- Circles.Pathfinder/V2Pathfinder.cs | 4 +++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Circles.Pathfinder/Graphs/FlowGraph.cs b/Circles.Pathfinder/Graphs/FlowGraph.cs index 5a44936..730daa7 100644 --- a/Circles.Pathfinder/Graphs/FlowGraph.cs +++ b/Circles.Pathfinder/Graphs/FlowGraph.cs @@ -186,7 +186,14 @@ public void AddCapacityEdge(CapacityGraph capacityGraph, CapacityEdge capacityEd } } - public List> ExtractPathsWithFlow(string sourceNode, string sinkNode) + /// + /// Searches the graph for liquid paths from the source node to the sink node. + /// + /// The source + /// The sink + /// Only consider edges with more or equal flow + /// A list of paths with flow + public List> ExtractPathsWithFlow(string sourceNode, string sinkNode, BigInteger threshold) { var resultPaths = new List>(); var visited = new HashSet(); @@ -209,6 +216,12 @@ void Dfs(string currentNode, List currentPath) if (edge.Flow > 0 && !visited.Contains(edge.To)) { currentPath.Add(edge); // Add edge to the current path + if (edge.Flow < threshold) + { + // Filter edges with less flow than the threshold + continue; + } + Dfs(edge.To, currentPath); // Recursively go deeper currentPath.Remove(edge); // Backtrack } diff --git a/Circles.Pathfinder/V2Pathfinder.cs b/Circles.Pathfinder/V2Pathfinder.cs index 5e241a6..32dd05a 100644 --- a/Circles.Pathfinder/V2Pathfinder.cs +++ b/Circles.Pathfinder/V2Pathfinder.cs @@ -64,7 +64,9 @@ public async Task ComputeMaxFlow(FlowRequest request) var maxFlow = flowGraph.ComputeMaxFlowWithPaths(request.Source, request.Sink, targetFlow); // Extract Paths with Flow - var pathsWithFlow = flowGraph.ExtractPathsWithFlow(request.Source, request.Sink); + // (Don't consider paths smaller than 0.1 Circles) + var pathsWithFlow = + flowGraph.ExtractPathsWithFlow(request.Source, request.Sink, BigInteger.Parse("100000000000000000")); // Collapse balance nodes to get a collapsed graph var collapsedGraph = CollapseBalanceNodes(pathsWithFlow); From 3f11066955484befa9faafb3330cd80d17786e66 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 24 Oct 2024 04:13:22 +0200 Subject: [PATCH 5/5] keep try{}catch --- Circles.Pathfinder/V2Pathfinder.cs | 3 ++- docker-compose.gnosis.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Circles.Pathfinder/V2Pathfinder.cs b/Circles.Pathfinder/V2Pathfinder.cs index 32dd05a..f6bf159 100644 --- a/Circles.Pathfinder/V2Pathfinder.cs +++ b/Circles.Pathfinder/V2Pathfinder.cs @@ -148,7 +148,8 @@ private FlowGraph CollapseBalanceNodes(List> pathsWithFlow) currentEdge.CurrentCapacity // Adjust as needed ) { - Flow = mergedFlow + Flow = mergedFlow, + ReverseEdge = nextEdge.ReverseEdge }; try { diff --git a/docker-compose.gnosis.yml b/docker-compose.gnosis.yml index a5ed39d..40de40f 100644 --- a/docker-compose.gnosis.yml +++ b/docker-compose.gnosis.yml @@ -2,7 +2,7 @@ services: nethermind-gnosis: build: context: . - dockerfile: x64.Dockerfile + dockerfile: x64.debug.Dockerfile restart: unless-stopped depends_on: - postgres-gnosis