-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add basic support for #PickhardtPayments
work in progress, see #6
- Loading branch information
Showing
43 changed files
with
2,230 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
plugins { | ||
id 'lnd-manageJ.java-library-conventions' | ||
} | ||
|
||
dependencies { | ||
implementation project(':backend') | ||
implementation project(':caching') | ||
implementation project(':model') | ||
implementation project(':grpc-adapter') | ||
implementation 'com.google.ortools:ortools-java:9.2.9972' | ||
testImplementation testFixtures(project(':model')) | ||
testFixturesImplementation testFixtures(project(':model')) | ||
} |
70 changes: 70 additions & 0 deletions
70
pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/ArcInitializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package de.cotto.lndmanagej.pickhardtpayments; | ||
|
||
import com.google.ortools.graph.MinCostFlow; | ||
import de.cotto.lndmanagej.model.Coins; | ||
import de.cotto.lndmanagej.model.Pubkey; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Edge; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithCapacityInformation; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.IntegerMapping; | ||
|
||
import java.util.Collection; | ||
import java.util.Comparator; | ||
import java.util.Map; | ||
|
||
class ArcInitializer { | ||
|
||
private final MinCostFlow minCostFlow; | ||
private final IntegerMapping<Pubkey> pubkeyToIntegerMapping; | ||
private final Map<Integer, Edge> edgeMapping; | ||
private final long quantization; | ||
private final int piecewiseLinearApproximations; | ||
|
||
public ArcInitializer( | ||
MinCostFlow minCostFlow, | ||
IntegerMapping<Pubkey> integerMapping, | ||
Map<Integer, Edge> edgeMapping, | ||
long quantization, | ||
int piecewiseLinearApproximations | ||
) { | ||
this.minCostFlow = minCostFlow; | ||
this.pubkeyToIntegerMapping = integerMapping; | ||
this.edgeMapping = edgeMapping; | ||
this.quantization = quantization; | ||
this.piecewiseLinearApproximations = piecewiseLinearApproximations; | ||
} | ||
|
||
public void addArcs(Collection<EdgeWithCapacityInformation> edgesWithCapacityInformation) { | ||
Coins maximumCapacity = getMaximumCapacity(edgesWithCapacityInformation); | ||
for (EdgeWithCapacityInformation edgeWithCapacityInformation : edgesWithCapacityInformation) { | ||
addArcs(edgeWithCapacityInformation, maximumCapacity); | ||
} | ||
} | ||
|
||
private void addArcs(EdgeWithCapacityInformation edgeWithCapacityInformation, Coins maximumCapacity) { | ||
long capacitySat = edgeWithCapacityInformation.availableCapacity().satoshis(); | ||
if (capacitySat < quantization) { | ||
return; | ||
} | ||
int startNode = pubkeyToIntegerMapping.getMappedInteger(edgeWithCapacityInformation.edge().startNode()); | ||
int endNode = pubkeyToIntegerMapping.getMappedInteger(edgeWithCapacityInformation.edge().endNode()); | ||
long capacity = capacitySat / quantization; | ||
long unitCost = maximumCapacity.satoshis() / quantization / capacity; | ||
long capacityPiece = capacity / piecewiseLinearApproximations; | ||
for (int i = 1; i <= piecewiseLinearApproximations; i++) { | ||
int arcIndex = minCostFlow.addArcWithCapacityAndUnitCost( | ||
startNode, | ||
endNode, | ||
capacityPiece, | ||
i * unitCost | ||
); | ||
edgeMapping.put(arcIndex, edgeWithCapacityInformation.edge()); | ||
} | ||
} | ||
|
||
private Coins getMaximumCapacity(Collection<EdgeWithCapacityInformation> edgesWithCapacityInformation) { | ||
return edgesWithCapacityInformation.stream() | ||
.map(EdgeWithCapacityInformation::availableCapacity) | ||
.max(Comparator.naturalOrder()) | ||
.orElse(Coins.NONE); | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/FlowComputation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package de.cotto.lndmanagej.pickhardtpayments; | ||
|
||
import de.cotto.lndmanagej.grpc.GrpcGetInfo; | ||
import de.cotto.lndmanagej.grpc.GrpcGraph; | ||
import de.cotto.lndmanagej.model.ChannelId; | ||
import de.cotto.lndmanagej.model.Coins; | ||
import de.cotto.lndmanagej.model.DirectedChannelEdge; | ||
import de.cotto.lndmanagej.model.Pubkey; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Edge; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithCapacityInformation; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Flows; | ||
import de.cotto.lndmanagej.service.BalanceService; | ||
import de.cotto.lndmanagej.service.ChannelService; | ||
import de.cotto.lndmanagej.service.MissionControlService; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.LinkedHashSet; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
@Component | ||
public class FlowComputation { | ||
private final Logger logger = LoggerFactory.getLogger(getClass()); | ||
private final GrpcGraph grpcGraph; | ||
private final GrpcGetInfo grpcGetInfo; | ||
private final ChannelService channelService; | ||
private final BalanceService balanceService; | ||
private final MissionControlService missionControlService; | ||
private final long quantization; | ||
private final int piecewiseLinearApproximations; | ||
|
||
public FlowComputation( | ||
GrpcGraph grpcGraph, | ||
GrpcGetInfo grpcGetInfo, | ||
ChannelService channelService, | ||
BalanceService balanceService, | ||
MissionControlService missionControlService, | ||
@Value("${lndmanagej.pickhardtpayments.quantization:10000}") long quantization, | ||
@Value("${lndmanagej.pickhardtpayments.piecewiseLinearApproximations:5}") int piecewiseLinearApproximations | ||
) { | ||
this.grpcGraph = grpcGraph; | ||
this.grpcGetInfo = grpcGetInfo; | ||
this.channelService = channelService; | ||
this.balanceService = balanceService; | ||
this.missionControlService = missionControlService; | ||
this.quantization = quantization; | ||
this.piecewiseLinearApproximations = piecewiseLinearApproximations; | ||
} | ||
|
||
public Flows getOptimalFlows(Pubkey source, Pubkey target, Coins amount) { | ||
MinCostFlowSolver minCostFlowSolver = new MinCostFlowSolver( | ||
getEdges(), | ||
Map.of(source, amount), | ||
Map.of(target, amount), | ||
quantization, piecewiseLinearApproximations | ||
); | ||
return minCostFlowSolver.solve(); | ||
} | ||
|
||
private Set<EdgeWithCapacityInformation> getEdges() { | ||
Set<DirectedChannelEdge> channelEdges = grpcGraph.getChannelEdges().orElse(null); | ||
if (channelEdges == null) { | ||
logger.warn("Unable to get graph"); | ||
return Set.of(); | ||
} | ||
Set<EdgeWithCapacityInformation> edgesWithCapacityInformation = new LinkedHashSet<>(); | ||
Pubkey ownPubkey = grpcGetInfo.getPubkey(); | ||
for (DirectedChannelEdge channelEdge : channelEdges) { | ||
if (!channelEdge.policy().enabled()) { | ||
continue; | ||
} | ||
ChannelId channelId = channelEdge.channelId(); | ||
Pubkey pubkey1 = channelEdge.source(); | ||
Pubkey pubkey2 = channelEdge.target(); | ||
Edge edge = new Edge(channelId, pubkey1, pubkey2, channelEdge.capacity()); | ||
Coins availableCapacity = getAvailableCapacity(channelEdge, ownPubkey); | ||
EdgeWithCapacityInformation edgeWithCapacityInformation = | ||
new EdgeWithCapacityInformation(edge, availableCapacity); | ||
edgesWithCapacityInformation.add(edgeWithCapacityInformation); | ||
} | ||
return edgesWithCapacityInformation; | ||
} | ||
|
||
private Coins getAvailableCapacity(DirectedChannelEdge channelEdge, Pubkey ownPubKey) { | ||
Pubkey source = channelEdge.source(); | ||
Coins capacity = channelEdge.capacity(); | ||
ChannelId channelId = channelEdge.channelId(); | ||
if (ownPubKey.equals(source)) { | ||
return getLocalChannelAvailableLocal(capacity, channelId); | ||
} | ||
Pubkey target = channelEdge.target(); | ||
if (ownPubKey.equals(target)) { | ||
return getLocalChannelAvailableRemote(capacity, channelId); | ||
} | ||
Coins failureAmount = missionControlService.getMinimumOfRecentFailures(source, target).orElse(null); | ||
if (failureAmount == null) { | ||
return capacity; | ||
} | ||
return getAvailableUpperBoundBelowRecentFailure(capacity, failureAmount); | ||
} | ||
|
||
private Coins getAvailableUpperBoundBelowRecentFailure(Coins capacity, Coins failureAmount) { | ||
long satsCapacity = capacity.satoshis(); | ||
long satsNotAvailable = failureAmount.milliSatoshis() / 1_000; | ||
long satsAvailable = Math.max(Math.min(satsNotAvailable - 1, satsCapacity), 0); | ||
return Coins.ofSatoshis(satsAvailable); | ||
} | ||
|
||
private Coins getLocalChannelAvailableLocal(Coins capacity, ChannelId channelId) { | ||
return channelService.getLocalChannel(channelId) | ||
.map(c -> balanceService.getAvailableLocalBalance(channelId)) | ||
.orElse(capacity); | ||
} | ||
|
||
private Coins getLocalChannelAvailableRemote(Coins capacity, ChannelId channelId) { | ||
return channelService.getLocalChannel(channelId) | ||
.map(c -> balanceService.getAvailableRemoteBalance(channelId)) | ||
.orElse(capacity); | ||
} | ||
} |
91 changes: 91 additions & 0 deletions
91
...hardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/MinCostFlowSolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package de.cotto.lndmanagej.pickhardtpayments; | ||
|
||
import com.google.ortools.Loader; | ||
import com.google.ortools.graph.MinCostFlow; | ||
import com.google.ortools.graph.MinCostFlowBase; | ||
import de.cotto.lndmanagej.model.Coins; | ||
import de.cotto.lndmanagej.model.Pubkey; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Edge; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.EdgeWithCapacityInformation; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Flow; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.Flows; | ||
import de.cotto.lndmanagej.pickhardtpayments.model.IntegerMapping; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.Collection; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
||
import static com.google.ortools.graph.MinCostFlowBase.Status.OPTIMAL; | ||
|
||
class MinCostFlowSolver { | ||
private final Logger logger = LoggerFactory.getLogger(getClass()); | ||
private final MinCostFlow minCostFlow = new MinCostFlow(); | ||
private final IntegerMapping<Pubkey> integerMapping = new IntegerMapping<>(); | ||
private final Map<Integer, Edge> edgeMapping = new LinkedHashMap<>(); | ||
private final long quantization; | ||
|
||
static { | ||
Loader.loadNativeLibraries(); | ||
} | ||
|
||
public MinCostFlowSolver( | ||
Collection<EdgeWithCapacityInformation> edgesWithCapacityInformation, | ||
Map<Pubkey, Coins> sources, | ||
Map<Pubkey, Coins> sinks, | ||
long quantization, | ||
int piecewiseLinearApproximations | ||
) { | ||
this.quantization = quantization; | ||
ArcInitializer arcInitializer = new ArcInitializer( | ||
minCostFlow, | ||
integerMapping, | ||
edgeMapping, | ||
quantization, | ||
piecewiseLinearApproximations | ||
); | ||
arcInitializer.addArcs(edgesWithCapacityInformation); | ||
setSupply(sources, sinks); | ||
} | ||
|
||
public Flows solve() { | ||
MinCostFlowBase.Status status = minCostFlow.solve(); | ||
if (status != OPTIMAL) { | ||
logger.warn("Solving the min cost flow problem failed. Solver status: {}", status); | ||
return new Flows(); | ||
} | ||
Flows flows = new Flows(); | ||
for (int i = 0; i < minCostFlow.getNumArcs(); i++) { | ||
long flowAmount = minCostFlow.getFlow(i); | ||
if (flowAmount == 0) { | ||
continue; | ||
} | ||
Edge edge = Objects.requireNonNull(edgeMapping.get(i)); | ||
Flow flow = new Flow(edge, Coins.ofSatoshis(flowAmount * quantization)); | ||
flows.add(flow); | ||
} | ||
return flows; | ||
} | ||
|
||
private void setSupply(Map<Pubkey, Coins> sources, Map<Pubkey, Coins> sinks) { | ||
Coins totalSourceAmount = sources.values().stream().reduce(Coins::add).orElse(Coins.NONE); | ||
Coins totalSinkAmount = sinks.values().stream().reduce(Coins::add).orElse(Coins.NONE); | ||
if (!totalSourceAmount.equals(totalSinkAmount)) { | ||
throw new IllegalArgumentException( | ||
"Source and sink amounts are different, got " + totalSourceAmount + " and " + totalSinkAmount | ||
); | ||
} | ||
for (Map.Entry<Pubkey, Coins> entry : sources.entrySet()) { | ||
Pubkey node = entry.getKey(); | ||
long supply = entry.getValue().satoshis() / quantization; | ||
minCostFlow.setNodeSupply(integerMapping.getMappedInteger(node), supply); | ||
} | ||
for (Map.Entry<Pubkey, Coins> entry : sinks.entrySet()) { | ||
Pubkey node = entry.getKey(); | ||
long supply = -entry.getValue().satoshis() / quantization; | ||
minCostFlow.setNodeSupply(integerMapping.getMappedInteger(node), supply); | ||
} | ||
} | ||
} |
Oops, something went wrong.