Skip to content

Commit

Permalink
add basic support for #PickhardtPayments
Browse files Browse the repository at this point in the history
work in progress, see #6
  • Loading branch information
C-Otto committed Mar 20, 2022
1 parent f5f3c34 commit 8804a28
Show file tree
Hide file tree
Showing 43 changed files with 2,230 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import de.cotto.lndmanagej.model.Pubkey;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -62,16 +61,6 @@ private Optional<Map<Pubkey, Map<Pubkey, Coins>>> populateFailureMapFromMissionC

private void setMinimum(Map<Pubkey, Map<Pubkey, Coins>> map, Pubkey source, Pubkey target, Coins amount) {
Map<Pubkey, Coins> innerMap = map.compute(source, (k, v) -> v == null ? new LinkedHashMap<>() : v);
innerMap.compute(target, (k, v) -> minimum(v, amount));
}

private Coins minimum(@Nullable Coins existingValue, Coins newAmount) {
if (existingValue == null) {
return newAmount;
}
if (existingValue.compareTo(newAmount) <= 0) {
return existingValue;
}
return newAmount;
innerMap.compute(target, (k, v) -> amount.minimum(v));
}
}
4 changes: 4 additions & 0 deletions model/src/main/java/de/cotto/lndmanagej/model/Coins.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public Coins minimum(@Nullable Coins other) {
return other;
}

public Coins negate() {
return Coins.ofMilliSatoshis(-milliSatoshis);
}

public boolean isPositive() {
return compareTo(NONE) > 0;
}
Expand Down
15 changes: 15 additions & 0 deletions model/src/test/java/de/cotto/lndmanagej/model/CoinsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,19 @@ void minimum_same_prefers_this() {
void minimum_null() {
assertThat(Coins.ofMilliSatoshis(1).minimum(null)).isEqualTo(Coins.ofMilliSatoshis(1));
}

@Test
void negate_zero() {
assertThat(Coins.NONE.negate()).isEqualTo(Coins.NONE);
}

@Test
void negate_positive() {
assertThat(Coins.ofMilliSatoshis(123).negate()).isEqualTo(Coins.ofMilliSatoshis(-123));
}

@Test
void negate_negative() {
assertThat(Coins.ofSatoshis(-1).negate()).isEqualTo(Coins.ofMilliSatoshis(1_000));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ public class ChannelIdFixtures {
public static final String CHANNEL_ID_COMPACT_2 = "799999x456x2";
public static final String CHANNEL_ID_COMPACT_3 = "799999x456x3";
public static final String CHANNEL_ID_COMPACT_4 = "799999x456x4";
public static final String CHANNEL_ID_COMPACT_5 = "799999x456x5";
public static final ChannelId CHANNEL_ID = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT);
public static final ChannelId CHANNEL_ID_2 = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT_2);
public static final ChannelId CHANNEL_ID_3 = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT_3);
public static final ChannelId CHANNEL_ID_4 = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT_4);
public static final ChannelId CHANNEL_ID_5 = ChannelId.fromCompactForm(CHANNEL_ID_COMPACT_5);
public static final long CHANNEL_ID_SHORT = CHANNEL_ID.getShortChannelId();
public static final long CHANNEL_ID_2_SHORT = CHANNEL_ID_2.getShortChannelId();
}
13 changes: 13 additions & 0 deletions pickhardt-payments/build.gradle
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'))
}
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);
}
}
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);
}
}
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);
}
}
}
Loading

0 comments on commit 8804a28

Please sign in to comment.