Skip to content

Commit 36cd036

Browse files
committed
routing: add outgoingFromIncoming amount calc
Adds a utility function to be able to compute the outgoing routing amount from the incoming amount by taking inbound and outbound fees into account. The discussion was contributed by user feelancer21, see feelancer21@f6f05fa.
1 parent 2c79bf9 commit 36cd036

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed

routing/router.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"math"
8+
"math/big"
89
"sort"
910
"sync"
1011
"sync/atomic"
@@ -836,6 +837,7 @@ func generateSphinxPacket(rt *route.Route, paymentHash []byte,
836837
hopCopy := sphinxPath[i]
837838
path[i] = hopCopy
838839
}
840+
839841
return spew.Sdump(path)
840842
}),
841843
)
@@ -1670,3 +1672,224 @@ func receiverAmtForwardPass(runningAmt lnwire.MilliSatoshi,
16701672

16711673
return runningAmt, nil
16721674
}
1675+
1676+
// incomingFromOutgoing computes the incoming amount based on the outgoing
1677+
// amount by adding fees to the outgoing amount, replicating the path finding
1678+
// and routing process, see also CheckHtlcForward.
1679+
func incomingFromOutgoing(outgoingAmt lnwire.MilliSatoshi,
1680+
incoming, outgoing *unifiedEdge) lnwire.MilliSatoshi {
1681+
1682+
outgoingFee := outgoing.policy.ComputeFee(outgoingAmt)
1683+
1684+
// Net amount is the amount the inbound fees are calculated with.
1685+
netAmount := outgoingAmt + outgoingFee
1686+
1687+
inboundFee := incoming.inboundFees.CalcFee(netAmount)
1688+
1689+
// The inbound fee is not allowed to reduce the incoming amount below
1690+
// the outgoing amount.
1691+
if int64(outgoingFee)+inboundFee < 0 {
1692+
return outgoingAmt
1693+
}
1694+
1695+
return netAmount + lnwire.MilliSatoshi(inboundFee)
1696+
}
1697+
1698+
// outgoingFromIncoming computes the outgoing amount based on the incoming
1699+
// amount by subtracting fees from the incoming amount. Note that this is not
1700+
// exactly the inverse of incomingFromOutgoing, because of some rounding.
1701+
func outgoingFromIncoming(incomingAmt lnwire.MilliSatoshi,
1702+
incoming, outgoing *unifiedEdge) lnwire.MilliSatoshi {
1703+
1704+
// Convert all quantities to big.Int to be able to hande negative
1705+
// values. The formulas to compute the outgoing amount involve terms
1706+
// with PPM*PPM*A, which can easily overflow an int64.
1707+
A := big.NewInt(int64(incomingAmt))
1708+
Ro := big.NewInt(int64(outgoing.policy.FeeProportionalMillionths))
1709+
Bo := big.NewInt(int64(outgoing.policy.FeeBaseMSat))
1710+
Ri := big.NewInt(int64(incoming.inboundFees.Rate))
1711+
Bi := big.NewInt(int64(incoming.inboundFees.Base))
1712+
PPM := big.NewInt(1_000_000)
1713+
1714+
// The following discussion was contributed by user feelancer21, see
1715+
//nolint:lll
1716+
// https://github.com/feelancer21/lnd/commit/f6f05fa930985aac0d27c3f6681aada1b599162a.
1717+
1718+
// The incoming amount Ai based on the outgoing amount Ao is computed by
1719+
// Ai = max(Ai(Ao), Ao), which caps the incoming amount such that the
1720+
// total node fee (Ai - Ao) is non-negative. This is commonly enforced
1721+
// by routing nodes.
1722+
1723+
// The function Ai(Ao) is given by:
1724+
// Ai(Ao) = (Ao + Bo + Ro/PPM) + (Bi + (Ao + Ro/PPM + Bo)*Ri/PPM), where
1725+
// the first term is the net amount (the outgoing amount plus the
1726+
// outbound fee), and the second is the inbound fee computed based on
1727+
// the net amount.
1728+
1729+
// Ai(Ao) can potentially become more negative in absolute value than
1730+
// Ao, which is why the above mentioned capping is needed. We can
1731+
// abbreviate Ai(Ao) with Ai(Ao) = m*Ao + n, where m and n are:
1732+
// m := (1 + Ro/PPM) * (1 + Ri/PPM)
1733+
// n := Bi + Bo*(1 + Ri/PPM)
1734+
1735+
// If we know that m > 0, this is equivalent of Ri/PPM > -1, because Ri
1736+
// is the only factor that can become negative. A value or Ri/PPM = -1,
1737+
// means that the routing node is willing to give up on 100% of the
1738+
// net amount (based on the fee rate), which is likely to not happen in
1739+
// practice. This condition will be important for a later trick.
1740+
1741+
// If we want to compute the incoming amount based on the outgoing
1742+
// amount, which is the reverse problem, we need to solve Ai =
1743+
// max(Ai(Ao), Ao) for Ao(Ai). Given an incoming amount A,
1744+
// we look for an Ao such that A = max(Ai(Ao), Ao).
1745+
1746+
// The max function separates this into two cases. The case to take is
1747+
// not clear yet, because we don't know Ao, but later we see a trick
1748+
// how to determine which case is the one to take.
1749+
1750+
// first case: Ai(Ao) <= Ao:
1751+
// Therefore, A = max(Ai(Ao), Ao) = Ao, we find Ao = A.
1752+
// This also leads to Ai(A) <= A by substitution into the condition.
1753+
1754+
// second case: Ai(Ao) > Ao:
1755+
// Therefore, A = max(Ai(Ao), Ao) = Ai(Ao) = m*Ao + n. Solving for Ao
1756+
// gives Ao = (A - n)/m.
1757+
//
1758+
// We know
1759+
// Ai(Ao) > Ao <=> A = Ai(Ao) > Ao = (A - n)/m,
1760+
// so A > (A - n)/m.
1761+
//
1762+
// **Assuming m > 0**, by multiplying with m, we can transform this to
1763+
// A * m + n > A.
1764+
//
1765+
// We know Ai(A) = A*m + n, therefore Ai(A) > A.
1766+
//
1767+
// This means that if we apply the incoming amount calculation to the
1768+
// **incoming** amount, and this condition holds, then we know that we
1769+
// deal with the second case, being able to compute the outgoing amount
1770+
// based off the formula Ao = (A - n)/m, otherwise we will just return
1771+
// the incoming amount.
1772+
1773+
// In case the inbound fee rate is less than -1 (-100%), we fail to
1774+
// compute the outbound amount and return the incoming amount. This also
1775+
// protects against zero division later.
1776+
1777+
// We compute m in terms of big.Int to be safe from overflows and to be
1778+
// consistent with later calculations.
1779+
// m := (PPM*PPM + Ri*PPM + Ro*PPM + Ro*Ri)/(PPM*PPM)
1780+
1781+
// Compute terms in (PPM*PPM + Ri*PPM + Ro*PPM + Ro*Ri).
1782+
m1 := new(big.Int).Mul(PPM, PPM)
1783+
m2 := new(big.Int).Mul(Ri, PPM)
1784+
m3 := new(big.Int).Mul(Ro, PPM)
1785+
m4 := new(big.Int).Mul(Ro, Ri)
1786+
1787+
// Add up terms m1..m4.
1788+
m := big.NewInt(0)
1789+
m.Add(m, m1)
1790+
m.Add(m, m2)
1791+
m.Add(m, m3)
1792+
m.Add(m, m4)
1793+
1794+
// Since we compare to 0, we can multiply by PPM*PPM to avoid the
1795+
// division.
1796+
if m.Int64() <= 0 {
1797+
return incomingAmt
1798+
}
1799+
1800+
// In order to decide if the total fee is negative, we apply the fee
1801+
// to the *incoming* amount as mentioned before.
1802+
1803+
// We compute the test amount in terms of big.Int to be safe from
1804+
// overflows and to be consistent later calculations.
1805+
// testAmtF := A*m + n =
1806+
// = A + Bo + Bi + (PPM*(A*Ri + A*Ro + Ro*Ri) + A*Ri*Ro)/(PPM*PPM)
1807+
1808+
// Compute terms in (A*Ri + A*Ro + Ro*Ri).
1809+
t1 := new(big.Int).Mul(A, Ri)
1810+
t2 := new(big.Int).Mul(A, Ro)
1811+
t3 := new(big.Int).Mul(Ro, Ri)
1812+
1813+
// Sum up terms t1-t3.
1814+
t4 := big.NewInt(0)
1815+
t4.Add(t4, t1)
1816+
t4.Add(t4, t2)
1817+
t4.Add(t4, t3)
1818+
1819+
// Compute PPM*(A*Ri + A*Ro + Ro*Ri).
1820+
t6 := new(big.Int).Mul(PPM, t4)
1821+
1822+
// Compute A*Ri*Ro.
1823+
t7 := new(big.Int).Mul(A, Ri)
1824+
t7.Mul(t7, Ro)
1825+
1826+
// Compute (PPM*(A*Ri + A*Ro + Ro*Ri) + A*Ri*Ro)/(PPM*PPM).
1827+
num := new(big.Int).Add(t6, t7)
1828+
denom := new(big.Int).Mul(PPM, PPM)
1829+
fraction := new(big.Int).Div(num, denom)
1830+
1831+
// Sum up all terms.
1832+
testAmt := big.NewInt(0)
1833+
testAmt.Add(testAmt, A)
1834+
testAmt.Add(testAmt, Bo)
1835+
testAmt.Add(testAmt, Bi)
1836+
testAmt.Add(testAmt, fraction)
1837+
1838+
// Protect against negative values for the integer cast to Msat.
1839+
if testAmt.Int64() < 0 {
1840+
return incomingAmt
1841+
}
1842+
1843+
// If the second case holds, we have to compute the outgoing amount.
1844+
if lnwire.MilliSatoshi(testAmt.Int64()) > incomingAmt {
1845+
// Compute the outgoing amount by integer ceiling division. This
1846+
// precision is needed because PPM*PPM*A and other terms can
1847+
// easily overflow with int64, which happens with about
1848+
// A = 10_000 sat.
1849+
1850+
// out := (A - n) / m = numerator / denominator
1851+
// numerator := PPM*(PPM*(A - Bo - Bi) - Bo*Ri)
1852+
// denominator := PPM*(PPM + Ri + Ro) + Ri*Ro
1853+
1854+
var numerator big.Int
1855+
1856+
// Compute (A - Bo - Bi).
1857+
temp1 := new(big.Int).Sub(A, Bo)
1858+
temp2 := new(big.Int).Sub(temp1, Bi)
1859+
1860+
// Compute terms in (PPM*(A - Bo - Bi) - Bo*Ri).
1861+
temp3 := new(big.Int).Mul(PPM, temp2)
1862+
temp4 := new(big.Int).Mul(Bo, Ri)
1863+
1864+
// Compute PPM*(PPM*(A - Bo - Bi) - Bo*Ri)
1865+
temp5 := new(big.Int).Sub(temp3, temp4)
1866+
numerator.Mul(PPM, temp5)
1867+
1868+
var denominator big.Int
1869+
1870+
// Compute (PPM + Ri + Ro).
1871+
temp1 = new(big.Int).Add(PPM, Ri)
1872+
temp2 = new(big.Int).Add(temp1, Ro)
1873+
1874+
// Compute PPM*(PPM + Ri + Ro) + Ri*Ro.
1875+
temp3 = new(big.Int).Mul(PPM, temp2)
1876+
temp4 = new(big.Int).Mul(Ri, Ro)
1877+
denominator.Add(temp3, temp4)
1878+
1879+
// We overestimate the outgoing amount by taking the ceiling of
1880+
// the division. This means that we may round slightly up by a
1881+
// MilliSatoshi, but this helps to ensure that we don't hit min
1882+
// HTLC constrains in the context of finding the minimum amount
1883+
// of a route.
1884+
// ceil = floor((numerator + denominator - 1) / denominator)
1885+
ceil := new(big.Int).Add(&numerator, &denominator)
1886+
ceil.Sub(ceil, big.NewInt(1))
1887+
ceil.Div(ceil, &denominator)
1888+
1889+
return lnwire.MilliSatoshi(ceil.Int64())
1890+
}
1891+
1892+
// Otherwise the inbound fee made up for the outbound fee, which is why
1893+
// we just return the incoming amount.
1894+
return incomingAmt
1895+
}

routing/router_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,156 @@ func TestSenderAmtBackwardPass(t *testing.T) {
18861886
require.Equal(t, testReceiverAmt, receiverAmt)
18871887
}
18881888

1889+
// TestInboundOutbound tests the functions that computes the incoming and
1890+
// outgoing amounts based on the fees of the incoming and outgoing channels.
1891+
func TestInboundOutbound(t *testing.T) {
1892+
var outgoingAmt uint64 = 10_000_000
1893+
1894+
tests := []struct {
1895+
name string
1896+
incomingBase int32
1897+
incomingRate int32
1898+
outgoingBase uint64
1899+
outgoingRate uint64
1900+
}{
1901+
{
1902+
name: "only outbound fee",
1903+
incomingBase: 0,
1904+
incomingRate: 0,
1905+
outgoingBase: 20,
1906+
outgoingRate: 100,
1907+
},
1908+
{
1909+
name: "positive inbound and outbound fee",
1910+
incomingBase: 20,
1911+
incomingRate: 100,
1912+
outgoingBase: 20,
1913+
outgoingRate: 100,
1914+
},
1915+
{
1916+
name: "small negative inbound and outbound fee",
1917+
incomingBase: -10,
1918+
incomingRate: -50,
1919+
outgoingBase: 20,
1920+
outgoingRate: 100,
1921+
},
1922+
{
1923+
name: "equal negative inbound and outbound fee",
1924+
incomingBase: -20,
1925+
incomingRate: -100,
1926+
outgoingBase: 20,
1927+
outgoingRate: 100,
1928+
},
1929+
{
1930+
name: "large negative inbound and outbound fee",
1931+
incomingBase: -30,
1932+
incomingRate: -200,
1933+
outgoingBase: 20,
1934+
outgoingRate: 100,
1935+
},
1936+
{
1937+
name: "order of PPM negative inbound and " +
1938+
"outbound fee (m=0)",
1939+
incomingBase: -30,
1940+
incomingRate: -1_000_000,
1941+
outgoingBase: 20,
1942+
outgoingRate: 100,
1943+
},
1944+
{
1945+
name: "huge negative inbound and " +
1946+
"outbound fee (m<0)",
1947+
incomingBase: -30,
1948+
incomingRate: -2_000_000,
1949+
outgoingBase: 20,
1950+
outgoingRate: 100,
1951+
},
1952+
}
1953+
1954+
for _, tc := range tests {
1955+
tc := tc
1956+
1957+
t.Run(tc.name, func(tt *testing.T) {
1958+
testInboundOutboundFee(
1959+
tt, outgoingAmt, tc.incomingBase,
1960+
tc.incomingRate, tc.outgoingBase,
1961+
tc.outgoingRate,
1962+
)
1963+
})
1964+
}
1965+
}
1966+
1967+
// testInboundOutboundFee is a helper function that tests the outgoing and
1968+
// incoming amount relationship.
1969+
func testInboundOutboundFee(t *testing.T, outgoingAmt uint64, inBase,
1970+
inRate int32, outBase, outRate uint64) {
1971+
1972+
debugStr := fmt.Sprintf(
1973+
"outAmt=%d, inBase=%d, inRate=%d, outBase=%d, outRate=%d",
1974+
outgoingAmt, inBase, inRate, outBase, outRate,
1975+
)
1976+
1977+
incomingEdge := &unifiedEdge{
1978+
policy: &models.CachedEdgePolicy{},
1979+
inboundFees: models.InboundFee{
1980+
Base: inBase,
1981+
Rate: inRate,
1982+
},
1983+
}
1984+
1985+
outgoingEdge := &unifiedEdge{
1986+
policy: &models.CachedEdgePolicy{
1987+
FeeBaseMSat: lnwire.MilliSatoshi(
1988+
outBase,
1989+
),
1990+
FeeProportionalMillionths: lnwire.MilliSatoshi(
1991+
outRate,
1992+
),
1993+
},
1994+
}
1995+
1996+
// We compute the incoming amount based on the outgoing amount, which
1997+
// mimicks the path finding process.
1998+
incomingAmt := incomingFromOutgoing(
1999+
lnwire.MilliSatoshi(outgoingAmt), incomingEdge,
2000+
outgoingEdge,
2001+
)
2002+
2003+
// We do the reverse and compute the outgoing amount based on the
2004+
// incoming amount.
2005+
outgoingAmtNew := outgoingFromIncoming(
2006+
incomingAmt, incomingEdge, outgoingEdge,
2007+
)
2008+
2009+
// We require that the incoming amount is always larger than or equal to
2010+
// the outgoing amount, because total fees (=incoming-outgoing) should
2011+
// not become negative.
2012+
require.GreaterOrEqual(
2013+
t, int64(incomingAmt), int64(outgoingAmtNew), debugStr,
2014+
"expected incomingAmt >= outgoingAmtNew",
2015+
)
2016+
2017+
// We check that up to rounding the amounts are equal.
2018+
require.InDelta(
2019+
t, int64(outgoingAmt), int64(outgoingAmtNew), 1.0, debugStr,
2020+
"expected |outgoingAmt - outgoingAmtNew | <= 1",
2021+
)
2022+
2023+
// If we round, the computed outgoing amount should be larger than the
2024+
// exact outgoing amount, to not hit any min HTLC limits.
2025+
require.GreaterOrEqual(
2026+
t, int64(outgoingAmtNew), int64(outgoingAmt), debugStr,
2027+
"expected outgoingAmtNew >= outgoingAmt",
2028+
)
2029+
}
2030+
2031+
// FuzzInboundOutbound tests the incoming and outgoing amount calculation
2032+
// functions with fuzzing.
2033+
func FuzzInboundOutboundFee(f *testing.F) {
2034+
f.Add(uint64(0), int32(0), int32(0), uint64(0), uint64(0))
2035+
2036+
f.Fuzz(testInboundOutboundFee)
2037+
}
2038+
18892039
// TestSendToRouteSkipTempErrSuccess validates a successful payment send.
18902040
func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
18912041
t.Parallel()

0 commit comments

Comments
 (0)