mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
Upgrade the randIntn function signature from int to int64 to prevent unpredictable integer truncation on 32-bit systems (e.g., Raspberry Pi). This ensures UnixNano timestamps in hideTimestamp and large amounts in hideAmount do not cause unexpected runtime failures when calling ForwardingHistory.
1687 lines
44 KiB
Go
1687 lines
44 KiB
Go
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
|
"github.com/lightninglabs/lightning-terminal/session"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/rpcperms"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/protobuf/proto"
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
// TestPrivacyMapper tests that the PrivacyMapper correctly intercepts specific
|
|
// RPC calls.
|
|
func TestPrivacyMapper(t *testing.T) {
|
|
outPoint := func(txid string, index uint32) string {
|
|
return fmt.Sprintf("%s:%d", txid, index)
|
|
}
|
|
|
|
// Define some transaction outpoints used for mapping.
|
|
//
|
|
// nolint:ll
|
|
clearTxID := "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
clearTxIDReveresed, err := chainhash.NewHashFromStr(clearTxID)
|
|
require.NoError(t, err)
|
|
|
|
// nolint:ll
|
|
obfusTxID0 := "097ef666a61919ff3413b3b701eae3a5cbac08f70c0ca567806e1fa6acbfe384"
|
|
require.NoError(t, err)
|
|
obfusOut0 := uint32(2161781494)
|
|
obfusTxID0Reversed, err := chainhash.NewHashFromStr(obfusTxID0)
|
|
require.NoError(t, err)
|
|
|
|
// nolint:ll
|
|
obfusTxID1 := "45ec471bfccb0b7b9a8bc4008248931c59ad994903e07b54f54821ea3ef5cc5c"
|
|
obfusOut1 := uint32(1642614131)
|
|
|
|
clearPending := &lnrpc.PendingChannelsResponse_PendingChannel{
|
|
RemoteNodePub: "01020304",
|
|
LocalBalance: 100_000,
|
|
RemoteBalance: 20_000,
|
|
LocalChanReserveSat: 1_000,
|
|
RemoteChanReserveSat: 1_000,
|
|
Capacity: 120_000,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 0,
|
|
),
|
|
Initiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
Memo: "something",
|
|
}
|
|
|
|
obfuscatedPending := &lnrpc.PendingChannelsResponse_PendingChannel{
|
|
RemoteNodePub: "c8134495",
|
|
LocalBalance: 95_100,
|
|
RemoteBalance: 19_000,
|
|
Capacity: 114_100,
|
|
ChannelPoint: outPoint(
|
|
obfusTxID0,
|
|
obfusOut0,
|
|
),
|
|
Initiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
Memo: "something",
|
|
}
|
|
|
|
// Define some preexisting mappings for the privacy mapper.
|
|
mapPreloadRealToPseudo := map[string]string{
|
|
"Tinker Bell's pub key": "a44ef01c3bff970ef495c",
|
|
"000000000000007b": "47deb774fc605c56",
|
|
"0000000000000141": "2fd42e84b9ffaaeb",
|
|
"00000000000002a6": "7859bf41241787c2",
|
|
"000000000000036c": "1320e5d25b7b5973",
|
|
clearTxID: obfusTxID0,
|
|
outPoint(clearTxID, 0): outPoint(obfusTxID0, obfusOut0),
|
|
outPoint(clearTxID, 1): outPoint(obfusTxID1, obfusOut1),
|
|
"01020304": "c8134495",
|
|
"secret-host.com": "sksiuekalkdoowurekdf",
|
|
}
|
|
|
|
var (
|
|
clearForwarding = &lnrpc.ForwardingHistoryResponse{
|
|
ForwardingEvents: []*lnrpc.ForwardingEvent{
|
|
{
|
|
AmtIn: 2_000,
|
|
AmtInMsat: 2_000_000,
|
|
AmtOut: 1_000,
|
|
AmtOutMsat: 1_000_000,
|
|
Fee: 1_000,
|
|
FeeMsat: 1_000_000,
|
|
Timestamp: 1_000,
|
|
TimestampNs: 1_000_000_000_000,
|
|
ChanIdIn: 123,
|
|
ChanIdOut: 321,
|
|
},
|
|
{
|
|
AmtIn: 3_000,
|
|
AmtInMsat: 3_000_000,
|
|
AmtOut: 2_000,
|
|
AmtOutMsat: 2_000_000,
|
|
Fee: 1_000,
|
|
FeeMsat: 1_000_000,
|
|
Timestamp: 1_000,
|
|
TimestampNs: 1_000_000_000_000,
|
|
ChanIdIn: 678,
|
|
ChanIdOut: 876,
|
|
},
|
|
},
|
|
}
|
|
|
|
clearListChannel = &lnrpc.ListChannelsResponse{
|
|
Channels: []*lnrpc.Channel{
|
|
{
|
|
Capacity: 1_000_000,
|
|
RemoteBalance: 600_000,
|
|
LocalBalance: 499_000,
|
|
CommitFee: 1_000,
|
|
TotalSatoshisSent: 500_000,
|
|
TotalSatoshisReceived: 450_000,
|
|
RemotePubkey: "01020304",
|
|
Initiator: false,
|
|
ChanId: 123,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 0,
|
|
),
|
|
PendingHtlcs: []*lnrpc.HTLC{
|
|
{HashLock: []byte("aaaa")},
|
|
{HashLock: []byte("bbbb")},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// nolint:ll
|
|
tests := []struct {
|
|
name string
|
|
privacyFlags session.PrivacyFlags
|
|
uri string
|
|
msgType rpcperms.InterceptType
|
|
msg proto.Message
|
|
expectedReplacement proto.Message
|
|
}{
|
|
{
|
|
name: "GetInfo Response",
|
|
uri: "/lnrpc.Lightning/GetInfo",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.GetInfoResponse{
|
|
Alias: "Tinker Bell",
|
|
IdentityPubkey: "Tinker Bell's pub key",
|
|
Uris: []string{
|
|
"Neverland 1",
|
|
"Neverland 2",
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.GetInfoResponse{
|
|
IdentityPubkey: "a44ef01c3bff970ef495c",
|
|
},
|
|
},
|
|
{
|
|
name: "GetInfo Response clear pubkey",
|
|
uri: "/lnrpc.Lightning/GetInfo",
|
|
msgType: rpcperms.TypeResponse,
|
|
privacyFlags: session.PrivacyFlags{
|
|
session.ClearPubkeys,
|
|
},
|
|
msg: &lnrpc.GetInfoResponse{
|
|
Alias: "Tinker Bell",
|
|
IdentityPubkey: "Tinker Bell's pub key",
|
|
Uris: []string{
|
|
"Neverland 1",
|
|
"Neverland 2",
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.GetInfoResponse{
|
|
IdentityPubkey: "Tinker Bell's pub key",
|
|
},
|
|
},
|
|
{
|
|
name: "ForwardingHistory Response clear",
|
|
uri: "/lnrpc.Lightning/ForwardingHistory",
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearChanIDs,
|
|
session.ClearAmounts,
|
|
session.ClearTimeStamps,
|
|
},
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: clearForwarding,
|
|
expectedReplacement: clearForwarding,
|
|
},
|
|
{
|
|
name: "ForwardingHistory Response",
|
|
uri: "/lnrpc.Lightning/ForwardingHistory",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: clearForwarding,
|
|
expectedReplacement: &lnrpc.ForwardingHistoryResponse{
|
|
ForwardingEvents: []*lnrpc.ForwardingEvent{
|
|
{
|
|
AmtIn: 1_900,
|
|
AmtInMsat: 1_900_200,
|
|
AmtOut: 950,
|
|
AmtOutMsat: 950_100,
|
|
Fee: 950,
|
|
FeeMsat: 950_100,
|
|
Timestamp: 400,
|
|
TimestampNs: 400_000_000_100,
|
|
ChanIdIn: 5178778334600911958,
|
|
ChanIdOut: 3446430762436373227,
|
|
},
|
|
{
|
|
AmtIn: 2_850,
|
|
AmtInMsat: 2_850_200,
|
|
AmtOut: 1_900,
|
|
AmtOutMsat: 1_900_100,
|
|
Fee: 950,
|
|
FeeMsat: 950_100,
|
|
Timestamp: 400,
|
|
TimestampNs: 400_000_000_100,
|
|
ChanIdIn: 8672172843977902018,
|
|
ChanIdOut: 1378354177616075123,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "FeeReport Response",
|
|
uri: "/lnrpc.Lightning/FeeReport",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.FeeReportResponse{
|
|
ChannelFees: []*lnrpc.ChannelFeeReport{
|
|
{
|
|
ChanId: 123,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 0,
|
|
),
|
|
},
|
|
{
|
|
ChanId: 321,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 1,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.FeeReportResponse{
|
|
ChannelFees: []*lnrpc.ChannelFeeReport{
|
|
{
|
|
ChanId: 5178778334600911958,
|
|
ChannelPoint: outPoint(
|
|
obfusTxID0, obfusOut0,
|
|
),
|
|
},
|
|
{
|
|
ChanId: 3446430762436373227,
|
|
ChannelPoint: outPoint(
|
|
obfusTxID1, obfusOut1,
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ListChannels Request",
|
|
uri: "/lnrpc.Lightning/ListChannels",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.ListChannelsRequest{
|
|
Peer: []byte{200, 19, 68, 149},
|
|
},
|
|
expectedReplacement: &lnrpc.ListChannelsRequest{
|
|
Peer: []byte{1, 2, 3, 4},
|
|
},
|
|
},
|
|
{
|
|
name: "ListChannels Response",
|
|
uri: "/lnrpc.Lightning/ListChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: clearListChannel,
|
|
expectedReplacement: &lnrpc.ListChannelsResponse{
|
|
Channels: []*lnrpc.Channel{
|
|
{
|
|
Capacity: 1_000_000,
|
|
RemoteBalance: 525_850,
|
|
LocalBalance: 474_150,
|
|
CommitFee: 1_000,
|
|
TotalSatoshisSent: 475_100,
|
|
TotalSatoshisReceived: 427_600,
|
|
RemotePubkey: "c8134495",
|
|
Initiator: true,
|
|
ChanId: 5178778334600911958,
|
|
ChannelPoint: outPoint(
|
|
obfusTxID0, obfusOut0,
|
|
),
|
|
PendingHtlcs: []*lnrpc.HTLC{{}, {}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ListChannels Response clear",
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
session.ClearChanIDs,
|
|
session.ClearAmounts,
|
|
session.ClearHTLCs,
|
|
session.ClearChanInitiator,
|
|
},
|
|
uri: "/lnrpc.Lightning/ListChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: clearListChannel,
|
|
expectedReplacement: clearListChannel,
|
|
},
|
|
{
|
|
name: "UpdateChannelPolicy Request txid string",
|
|
uri: "/lnrpc.Lightning/UpdateChannelPolicy",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.PolicyUpdateRequest{
|
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
ChanPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: obfusTxID0,
|
|
},
|
|
OutputIndex: obfusOut0,
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.PolicyUpdateRequest{
|
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
ChanPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: clearTxID,
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateChannelPolicy Request txid bytes",
|
|
uri: "/lnrpc.Lightning/UpdateChannelPolicy",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.PolicyUpdateRequest{
|
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
ChanPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
FundingTxidBytes: obfusTxID0Reversed[:],
|
|
},
|
|
OutputIndex: obfusOut0,
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.PolicyUpdateRequest{
|
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
ChanPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: clearTxID,
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateChannelPolicy Response",
|
|
uri: "/lnrpc.Lightning/UpdateChannelPolicy",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.PolicyUpdateResponse{
|
|
FailedUpdates: []*lnrpc.FailedUpdate{
|
|
{
|
|
Outpoint: &lnrpc.OutPoint{
|
|
TxidStr: clearTxID,
|
|
OutputIndex: 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.PolicyUpdateResponse{
|
|
// nolint:ll
|
|
FailedUpdates: []*lnrpc.FailedUpdate{
|
|
{
|
|
Outpoint: &lnrpc.OutPoint{
|
|
TxidStr: obfusTxID0,
|
|
OutputIndex: uint32(obfusOut0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "WalletBalance Response",
|
|
uri: "/lnrpc.Lightning/WalletBalance",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.WalletBalanceResponse{
|
|
TotalBalance: 1_000_000,
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
LockedBalance: 1_000_000,
|
|
ReservedBalanceAnchorChan: 1_000_000,
|
|
// nolint:ll
|
|
AccountBalance: map[string]*lnrpc.WalletAccountBalance{
|
|
"first": {
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
},
|
|
},
|
|
},
|
|
|
|
expectedReplacement: &lnrpc.WalletBalanceResponse{
|
|
TotalBalance: 950_100,
|
|
ConfirmedBalance: 950_100,
|
|
UnconfirmedBalance: 950_100,
|
|
LockedBalance: 950_100,
|
|
ReservedBalanceAnchorChan: 950_100,
|
|
// nolint:ll
|
|
AccountBalance: map[string]*lnrpc.WalletAccountBalance{
|
|
"first": {
|
|
ConfirmedBalance: 950_100,
|
|
UnconfirmedBalance: 950_100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "WalletBalance Response clear",
|
|
uri: "/lnrpc.Lightning/WalletBalance",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.WalletBalanceResponse{
|
|
TotalBalance: 1_000_000,
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
LockedBalance: 1_000_000,
|
|
ReservedBalanceAnchorChan: 1_000_000,
|
|
AccountBalance: map[string]*lnrpc.WalletAccountBalance{
|
|
"first": {
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
},
|
|
},
|
|
},
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearAmounts,
|
|
},
|
|
expectedReplacement: &lnrpc.WalletBalanceResponse{
|
|
TotalBalance: 1_000_000,
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
LockedBalance: 1_000_000,
|
|
ReservedBalanceAnchorChan: 1_000_000,
|
|
AccountBalance: map[string]*lnrpc.WalletAccountBalance{
|
|
"first": {
|
|
ConfirmedBalance: 1_000_000,
|
|
UnconfirmedBalance: 1_000_000,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ClosedChannels Response",
|
|
uri: "/lnrpc.Lightning/ClosedChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.ClosedChannelsResponse{
|
|
// nolint:ll
|
|
Channels: []*lnrpc.ChannelCloseSummary{
|
|
{
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 1,
|
|
),
|
|
ChanId: 123,
|
|
ClosingTxHash: clearTxID,
|
|
RemotePubkey: "01020304",
|
|
Capacity: 1_000_000,
|
|
SettledBalance: 500_000,
|
|
CloseType: lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE,
|
|
CloseInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
OpenInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
CloseHeight: 100_000,
|
|
Resolutions: []*lnrpc.Resolution{
|
|
{
|
|
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.ClosedChannelsResponse{
|
|
// nolint:ll
|
|
Channels: []*lnrpc.ChannelCloseSummary{
|
|
{
|
|
ChannelPoint: outPoint(
|
|
obfusTxID1, obfusOut1,
|
|
),
|
|
ChanId: 5178778334600911958,
|
|
ClosingTxHash: obfusTxID0,
|
|
RemotePubkey: "c8134495",
|
|
Capacity: 950_100,
|
|
SettledBalance: 475_100,
|
|
CloseType: lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE,
|
|
CloseInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
OpenInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ClosedChannels Response clear",
|
|
uri: "/lnrpc.Lightning/ClosedChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
// nolint:ll
|
|
msg: &lnrpc.ClosedChannelsResponse{
|
|
Channels: []*lnrpc.ChannelCloseSummary{
|
|
{
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 1,
|
|
),
|
|
ChanId: 123,
|
|
ClosingTxHash: clearTxID,
|
|
RemotePubkey: "01020304",
|
|
Capacity: 1_000_000,
|
|
SettledBalance: 500_000,
|
|
CloseType: lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE,
|
|
CloseInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
OpenInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
CloseHeight: 100_000,
|
|
Resolutions: []*lnrpc.Resolution{
|
|
{
|
|
ResolutionType: lnrpc.ResolutionType_ANCHOR,
|
|
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
session.ClearChanIDs,
|
|
session.ClearClosingTxIds,
|
|
session.ClearAmounts,
|
|
},
|
|
expectedReplacement: &lnrpc.ClosedChannelsResponse{
|
|
// nolint:ll
|
|
Channels: []*lnrpc.ChannelCloseSummary{
|
|
{
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 1,
|
|
),
|
|
ChanId: 123,
|
|
ClosingTxHash: clearTxID,
|
|
RemotePubkey: "01020304",
|
|
Capacity: 1_000_000,
|
|
SettledBalance: 500_000,
|
|
CloseType: lnrpc.ChannelCloseSummary_LOCAL_FORCE_CLOSE,
|
|
CloseInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
OpenInitiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "PendingChannels Response",
|
|
uri: "/lnrpc.Lightning/PendingChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
// nolint:ll
|
|
msg: &lnrpc.PendingChannelsResponse{
|
|
PendingOpenChannels: []*lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
|
{
|
|
CommitFee: 123,
|
|
Channel: clearPending,
|
|
},
|
|
},
|
|
PendingClosingChannels: []*lnrpc.PendingChannelsResponse_ClosedChannel{
|
|
{
|
|
Channel: clearPending,
|
|
ClosingTxid: clearTxID,
|
|
},
|
|
},
|
|
PendingForceClosingChannels: []*lnrpc.PendingChannelsResponse_ForceClosedChannel{
|
|
{
|
|
Channel: clearPending,
|
|
ClosingTxid: clearTxID,
|
|
LimboBalance: 100_000,
|
|
MaturityHeight: 123,
|
|
BlocksTilMaturity: 123,
|
|
RecoveredBalance: 100_000,
|
|
PendingHtlcs: []*lnrpc.PendingHTLC{
|
|
{
|
|
Incoming: true,
|
|
},
|
|
},
|
|
Anchor: 1,
|
|
},
|
|
},
|
|
WaitingCloseChannels: []*lnrpc.PendingChannelsResponse_WaitingCloseChannel{
|
|
{
|
|
Channel: clearPending,
|
|
LimboBalance: 100_000,
|
|
Commitments: &lnrpc.PendingChannelsResponse_Commitments{
|
|
LocalTxid: clearTxID,
|
|
},
|
|
ClosingTxid: clearTxID,
|
|
ClosingTxHex: clearTxID,
|
|
},
|
|
},
|
|
},
|
|
// nolint:ll
|
|
expectedReplacement: &lnrpc.PendingChannelsResponse{
|
|
PendingOpenChannels: []*lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
|
{
|
|
CommitFee: 123,
|
|
Channel: obfuscatedPending,
|
|
},
|
|
},
|
|
PendingClosingChannels: []*lnrpc.PendingChannelsResponse_ClosedChannel{
|
|
{
|
|
Channel: obfuscatedPending,
|
|
ClosingTxid: obfusTxID0,
|
|
},
|
|
},
|
|
PendingForceClosingChannels: []*lnrpc.PendingChannelsResponse_ForceClosedChannel{
|
|
{
|
|
Channel: obfuscatedPending,
|
|
ClosingTxid: obfusTxID0,
|
|
LimboBalance: 95_100,
|
|
MaturityHeight: 123,
|
|
BlocksTilMaturity: 123,
|
|
RecoveredBalance: 95_100,
|
|
PendingHtlcs: []*lnrpc.PendingHTLC{},
|
|
Anchor: 1,
|
|
},
|
|
},
|
|
WaitingCloseChannels: []*lnrpc.PendingChannelsResponse_WaitingCloseChannel{
|
|
{
|
|
Channel: obfuscatedPending,
|
|
LimboBalance: 95_100,
|
|
Commitments: &lnrpc.PendingChannelsResponse_Commitments{},
|
|
ClosingTxid: obfusTxID0,
|
|
ClosingTxHex: obfusTxID0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "PendingChannels Response clear",
|
|
uri: "/lnrpc.Lightning/PendingChannels",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.PendingChannelsResponse{
|
|
// nolint:ll
|
|
PendingOpenChannels: []*lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
|
{
|
|
CommitFee: 123,
|
|
Channel: &lnrpc.PendingChannelsResponse_PendingChannel{
|
|
RemoteNodePub: "01020304",
|
|
LocalBalance: 100_000,
|
|
RemoteBalance: 100_000,
|
|
Capacity: 120_000,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 0,
|
|
),
|
|
Initiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
Memo: "something",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
session.ClearAmounts,
|
|
session.ClearChanIDs,
|
|
},
|
|
// nolint:ll
|
|
expectedReplacement: &lnrpc.PendingChannelsResponse{
|
|
PendingOpenChannels: []*lnrpc.PendingChannelsResponse_PendingOpenChannel{
|
|
{
|
|
CommitFee: 123,
|
|
Channel: &lnrpc.PendingChannelsResponse_PendingChannel{
|
|
RemoteNodePub: "01020304",
|
|
LocalBalance: 100_000,
|
|
RemoteBalance: 100_000,
|
|
Capacity: 120_000,
|
|
ChannelPoint: outPoint(
|
|
clearTxID, 0,
|
|
),
|
|
Initiator: lnrpc.Initiator_INITIATOR_LOCAL,
|
|
Memo: "something",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "BatchOpenChannel Request",
|
|
uri: "/lnrpc.Lightning/BatchOpenChannel",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.BatchOpenChannelRequest{
|
|
TargetConf: 6,
|
|
Channels: []*lnrpc.BatchOpenChannel{
|
|
{
|
|
NodePubkey: []byte{
|
|
200, 19, 68, 149,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.BatchOpenChannelRequest{
|
|
TargetConf: 6,
|
|
Channels: []*lnrpc.BatchOpenChannel{
|
|
{
|
|
NodePubkey: []byte{
|
|
1, 2, 3, 4,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "BatchOpenChannel Response",
|
|
uri: "/lnrpc.Lightning/BatchOpenChannel",
|
|
msgType: rpcperms.TypeResponse,
|
|
// nolint:ll
|
|
msg: &lnrpc.BatchOpenChannelResponse{
|
|
PendingChannels: []*lnrpc.PendingUpdate{
|
|
{
|
|
Txid: clearTxIDReveresed[:],
|
|
OutputIndex: 0,
|
|
},
|
|
},
|
|
},
|
|
// nolint:ll
|
|
expectedReplacement: &lnrpc.BatchOpenChannelResponse{
|
|
PendingChannels: []*lnrpc.PendingUpdate{
|
|
{
|
|
Txid: obfusTxID0Reversed[:],
|
|
OutputIndex: obfusOut0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "OpenChannelSync Request",
|
|
uri: "/lnrpc.Lightning/OpenChannelSync",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.OpenChannelRequest{
|
|
NodePubkey: []byte{
|
|
200, 19, 68, 149,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
expectedReplacement: &lnrpc.OpenChannelRequest{
|
|
NodePubkey: []byte{
|
|
1, 2, 3, 4,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
},
|
|
{
|
|
name: "OpenChannelSync Request clear",
|
|
uri: "/lnrpc.Lightning/OpenChannelSync",
|
|
msgType: rpcperms.TypeRequest,
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
},
|
|
msg: &lnrpc.OpenChannelRequest{
|
|
NodePubkey: []byte{
|
|
200, 19, 68, 149,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
expectedReplacement: &lnrpc.OpenChannelRequest{
|
|
NodePubkey: []byte{
|
|
200, 19, 68, 149,
|
|
},
|
|
LocalFundingAmount: 1_000_000,
|
|
PushSat: 1_000_000,
|
|
MinHtlcMsat: 100,
|
|
},
|
|
},
|
|
{
|
|
name: "OpenChannelSync Response bytes",
|
|
uri: "/lnrpc.Lightning/OpenChannelSync",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: clearTxID,
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
expectedReplacement: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: obfusTxID0,
|
|
},
|
|
OutputIndex: obfusOut0,
|
|
},
|
|
},
|
|
{
|
|
name: "OpenChannelSync Response string",
|
|
uri: "/lnrpc.Lightning/OpenChannelSync",
|
|
msgType: rpcperms.TypeResponse,
|
|
msg: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
FundingTxidBytes: clearTxIDReveresed[:],
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
expectedReplacement: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
FundingTxidBytes: obfusTxID0Reversed[:],
|
|
},
|
|
OutputIndex: obfusOut0,
|
|
},
|
|
},
|
|
{
|
|
name: "OpenChannelSync Response clear",
|
|
uri: "/lnrpc.Lightning/OpenChannelSync",
|
|
msgType: rpcperms.TypeResponse,
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearChanIDs,
|
|
},
|
|
msg: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
FundingTxidBytes: clearTxIDReveresed[:],
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
expectedReplacement: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
|
|
FundingTxidBytes: clearTxIDReveresed[:],
|
|
},
|
|
OutputIndex: 0,
|
|
},
|
|
},
|
|
|
|
{
|
|
name: "ConnectPeer Request",
|
|
uri: "/lnrpc.Lightning/ConnectPeer",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.ConnectPeerRequest{
|
|
Addr: &lnrpc.LightningAddress{
|
|
Pubkey: "c8134495",
|
|
Host: "sksiuekalkdoowurekdf",
|
|
},
|
|
},
|
|
expectedReplacement: &lnrpc.ConnectPeerRequest{
|
|
Addr: &lnrpc.LightningAddress{
|
|
Pubkey: "01020304",
|
|
Host: "secret-host.com",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "ConnectPeer Request clear",
|
|
uri: "/lnrpc.Lightning/ConnectPeer",
|
|
msgType: rpcperms.TypeRequest,
|
|
msg: &lnrpc.ConnectPeerRequest{
|
|
Addr: &lnrpc.LightningAddress{
|
|
Pubkey: "c8134495",
|
|
Host: "secret-host.com",
|
|
},
|
|
},
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
session.ClearNetworkAddresses,
|
|
},
|
|
expectedReplacement: &lnrpc.ConnectPeerRequest{
|
|
Addr: &lnrpc.LightningAddress{
|
|
Pubkey: "c8134495",
|
|
Host: "secret-host.com",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
decodedID := &lnrpc.MacaroonId{
|
|
StorageId: []byte("123"),
|
|
}
|
|
b, err := proto.Marshal(decodedID)
|
|
require.NoError(t, err)
|
|
|
|
rawID := make([]byte, len(b)+1)
|
|
rawID[0] = byte(bakery.LatestVersion)
|
|
copy(rawID[1:], b)
|
|
|
|
mac, err := macaroon.New(
|
|
[]byte("123"), rawID, "", macaroon.V2,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
macBytes, err := mac.MarshalBinary()
|
|
require.NoError(t, err)
|
|
|
|
sessionID, err := session.IDFromMacaroon(mac)
|
|
require.NoError(t, err)
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
// Initialize privacy mapping.
|
|
db := newMockDB(t, mapPreloadRealToPseudo, sessionID)
|
|
|
|
pd := firewalldb.NewMockSessionDB()
|
|
pd.AddPair(sessionID, sessionID)
|
|
err = pd.AddPrivacyFlags(sessionID, test.privacyFlags)
|
|
require.NoError(t, err)
|
|
|
|
// randIntn is used for deterministic testing.
|
|
randIntn := func(n int64) (int64, error) {
|
|
return 100, nil
|
|
}
|
|
p := NewPrivacyMapper(db, randIntn, pd)
|
|
|
|
rawMsg, err := proto.Marshal(test.msg)
|
|
require.NoError(t, err)
|
|
|
|
md := make(metadata.MD)
|
|
session.AddToGRPCMetadata(md, sessionID)
|
|
|
|
interceptReq := &rpcperms.InterceptionRequest{
|
|
Type: test.msgType,
|
|
Macaroon: mac,
|
|
RawMacaroon: macBytes,
|
|
FullURI: test.uri,
|
|
ProtoSerialized: rawMsg,
|
|
ProtoTypeName: string(
|
|
proto.MessageName(test.msg),
|
|
),
|
|
CtxMetadataPairs: md,
|
|
}
|
|
|
|
mwReq, err := interceptReq.ToRPC(1, 2)
|
|
require.NoError(t, err)
|
|
|
|
resp, err := p.Intercept(context.Background(), mwReq)
|
|
require.NoError(t, err)
|
|
|
|
feedback := resp.GetFeedback()
|
|
if test.expectedReplacement == nil {
|
|
require.False(t, feedback.ReplaceResponse)
|
|
return
|
|
}
|
|
|
|
// Snippet to print the replacement for debugging, works
|
|
// only for specific test.
|
|
// mes := &lnrpc.PendingChannelsResponse{}
|
|
// err = proto.Unmarshal(
|
|
// feedback.ReplacementSerialized, mes,
|
|
// )
|
|
// require.NoError(t, err)
|
|
// t.Log(mes)
|
|
|
|
expectedRaw, err := proto.Marshal(
|
|
test.expectedReplacement,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(
|
|
t, expectedRaw, feedback.ReplacementSerialized,
|
|
)
|
|
})
|
|
}
|
|
|
|
// Subtest to test behavior with real randomness.
|
|
t.Run("Response with randomness", func(t *testing.T) {
|
|
// Initialize privacy mapping.
|
|
db := newMockDB(t, mapPreloadRealToPseudo, sessionID)
|
|
|
|
pd := firewalldb.NewMockSessionDB()
|
|
pd.AddPair(sessionID, sessionID)
|
|
err := pd.AddPrivacyFlags(sessionID, session.PrivacyFlags{})
|
|
require.NoError(t, err)
|
|
|
|
msg := &lnrpc.ForwardingHistoryResponse{
|
|
ForwardingEvents: []*lnrpc.ForwardingEvent{
|
|
{
|
|
AmtIn: 2_000,
|
|
AmtInMsat: 2_000_000,
|
|
AmtOut: 1_000,
|
|
AmtOutMsat: 1_000_000,
|
|
Fee: 0,
|
|
FeeMsat: 1,
|
|
Timestamp: 1_000_000,
|
|
TimestampNs: 1_000_000 * 1e9,
|
|
ChanIdIn: 123,
|
|
ChanIdOut: 321,
|
|
},
|
|
},
|
|
}
|
|
rawMsg, err := proto.Marshal(msg)
|
|
require.NoError(t, err)
|
|
|
|
p := NewPrivacyMapper(db, CryptoRandIntn, pd)
|
|
require.NoError(t, err)
|
|
|
|
// We test the independent outgoing amount (incoming amount
|
|
// would also be dependend on the fee variation).
|
|
amtOutMsat := msg.ForwardingEvents[0].AmtOutMsat
|
|
amtInterval := uint64(amountVariation * float64(amtOutMsat))
|
|
minAmt := amtOutMsat - amtInterval
|
|
maxAmt := amtOutMsat + amtInterval
|
|
|
|
// We keep track of the timestamp. We test only the timestamp in
|
|
// seconds as there can be numerical inaccuracies with the
|
|
// nanosecond one.
|
|
timestamp := msg.ForwardingEvents[0].TimestampNs / 1e9
|
|
timestampInterval := uint64(timeVariation) / 1e9
|
|
minTime := timestamp - timestampInterval
|
|
maxTime := timestamp + timestampInterval
|
|
|
|
// We need a certain number of samples to have statistical
|
|
// accuracy.
|
|
numSamples := 10_000
|
|
|
|
// We require a five percent accuracy for 10_000 samples.
|
|
relativeTestAccuracy := 0.05
|
|
|
|
amounts := make([]uint64, numSamples)
|
|
timestamps := make([]uint64, numSamples)
|
|
|
|
md := make(metadata.MD)
|
|
session.AddToGRPCMetadata(md, sessionID)
|
|
|
|
for i := 0; i < numSamples; i++ {
|
|
// nolint:ll
|
|
interceptReq := &rpcperms.InterceptionRequest{
|
|
Type: rpcperms.TypeResponse,
|
|
Macaroon: mac,
|
|
RawMacaroon: macBytes,
|
|
FullURI: "/lnrpc.Lightning/ForwardingHistory",
|
|
ProtoSerialized: rawMsg,
|
|
ProtoTypeName: string(
|
|
proto.MessageName(msg),
|
|
),
|
|
CtxMetadataPairs: md,
|
|
}
|
|
|
|
mwReq, err := interceptReq.ToRPC(1, 2)
|
|
require.NoError(t, err)
|
|
|
|
resp, err := p.Intercept(context.Background(), mwReq)
|
|
require.NoError(t, err)
|
|
|
|
feedback := resp.GetFeedback()
|
|
|
|
fw := &lnrpc.ForwardingHistoryResponse{}
|
|
err = proto.Unmarshal(
|
|
feedback.ReplacementSerialized, fw,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
amounts[i] = fw.ForwardingEvents[0].AmtOutMsat
|
|
require.LessOrEqual(t, amounts[i], maxAmt)
|
|
require.GreaterOrEqual(t, amounts[i], minAmt)
|
|
|
|
timestamps[i] = fw.ForwardingEvents[0].Timestamp
|
|
require.LessOrEqual(t, timestamps[i], maxTime)
|
|
require.GreaterOrEqual(t, timestamps[i], minTime)
|
|
}
|
|
|
|
// The formula for the expected variance is taken from
|
|
// https://en.wikipedia.org/wiki/Continuous_uniform_distribution
|
|
expectedVar := func(min, max uint64) uint64 {
|
|
return (max - min) * (max - min) / 12
|
|
}
|
|
|
|
// Test amounts for mean and variance.
|
|
expectedAmtVariance := expectedVar(minAmt, maxAmt)
|
|
require.InEpsilon(t, expectedAmtVariance, variance(amounts),
|
|
relativeTestAccuracy)
|
|
require.InEpsilon(t, amtOutMsat, mean(amounts),
|
|
relativeTestAccuracy)
|
|
|
|
// Test timestamps for mean and variance.
|
|
expectedTimeVariance := expectedVar(minTime, maxTime)
|
|
require.InEpsilon(t, expectedTimeVariance, variance(timestamps),
|
|
relativeTestAccuracy)
|
|
require.InEpsilon(t, timestamp, mean(timestamps),
|
|
relativeTestAccuracy)
|
|
})
|
|
}
|
|
|
|
type mockDB struct {
|
|
privDB map[string]*mockPrivacyMapDB
|
|
}
|
|
|
|
func newMockDB(t *testing.T, preloadRealToPseudo map[string]string,
|
|
sessID session.ID) mockDB {
|
|
|
|
db := mockDB{privDB: make(map[string]*mockPrivacyMapDB)}
|
|
sessDB := db.PrivacyDB(sessID)
|
|
|
|
_ = sessDB.Update(context.Background(), func(ctx context.Context,
|
|
tx firewalldb.PrivacyMapTx) error {
|
|
|
|
for r, p := range preloadRealToPseudo {
|
|
require.NoError(t, tx.NewPair(ctx, r, p))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return db
|
|
}
|
|
|
|
func (m mockDB) PrivacyDB(groupID session.ID) firewalldb.PrivacyMapDB {
|
|
db, ok := m.privDB[string(groupID[:])]
|
|
if ok {
|
|
return db
|
|
}
|
|
|
|
newDB := newMockPrivacyMapDB()
|
|
m.privDB[string(groupID[:])] = newDB
|
|
|
|
return newDB
|
|
}
|
|
|
|
func newMockPrivacyMapDB() *mockPrivacyMapDB {
|
|
return &mockPrivacyMapDB{
|
|
r2p: make(map[string]string),
|
|
p2r: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
type mockPrivacyMapDB struct {
|
|
r2p map[string]string
|
|
p2r map[string]string
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) Update(ctx context.Context,
|
|
f func(ctx context.Context, tx firewalldb.PrivacyMapTx) error) error {
|
|
|
|
return f(ctx, m)
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) View(ctx context.Context,
|
|
f func(ctx context.Context, tx firewalldb.PrivacyMapTx) error) error {
|
|
|
|
return f(ctx, m)
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) NewPair(_ context.Context, real,
|
|
pseudo string) error {
|
|
|
|
m.r2p[real] = pseudo
|
|
m.p2r[pseudo] = real
|
|
return nil
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) PseudoToReal(_ context.Context, pseudo string) (
|
|
string, error) {
|
|
|
|
r, ok := m.p2r[pseudo]
|
|
if !ok {
|
|
return "", firewalldb.ErrNoSuchKeyFound
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) RealToPseudo(_ context.Context, real string) (string,
|
|
error) {
|
|
|
|
p, ok := m.r2p[real]
|
|
if !ok {
|
|
return "", firewalldb.ErrNoSuchKeyFound
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (m *mockPrivacyMapDB) FetchAllPairs(_ context.Context) (
|
|
*firewalldb.PrivacyMapPairs, error) {
|
|
|
|
return firewalldb.NewPrivacyMapPairs(m.r2p), nil
|
|
}
|
|
|
|
var _ firewalldb.PrivacyMapDB = (*mockPrivacyMapDB)(nil)
|
|
|
|
// TestRandBetween tests random number generation for numbers in an interval.
|
|
func TestRandBetween(t *testing.T) {
|
|
var (
|
|
min int64
|
|
max int64 = 10
|
|
)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
val, err := randBetween(CryptoRandIntn, min, max)
|
|
require.NoError(t, err)
|
|
require.Less(t, val, max)
|
|
require.GreaterOrEqual(t, val, min)
|
|
}
|
|
}
|
|
|
|
// TestHideAmount tests that we hide amounts correctly.
|
|
func TestHideAmount(t *testing.T) {
|
|
testAmount := uint64(10_000)
|
|
relativeVariation := 0.05
|
|
absoluteVariation := int(float64(testAmount) * relativeVariation)
|
|
lowerBound := testAmount - uint64(absoluteVariation)
|
|
upperBound := testAmount + uint64(absoluteVariation)
|
|
|
|
tests := []struct {
|
|
name string
|
|
amount uint64
|
|
randIntFn func(int64) (int64, error)
|
|
expected uint64
|
|
}{
|
|
{
|
|
name: "zero test amount",
|
|
randIntFn: func(int64) (int64, error) { return 0, nil },
|
|
},
|
|
{
|
|
name: "test small amount",
|
|
randIntFn: func(int64) (int64, error) { return 0, nil },
|
|
amount: 1,
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "min value",
|
|
randIntFn: func(int64) (int64, error) { return 0, nil },
|
|
amount: testAmount,
|
|
expected: lowerBound,
|
|
},
|
|
{
|
|
name: "max value",
|
|
randIntFn: func(n int64) (int64, error) {
|
|
return int64(upperBound - lowerBound), nil
|
|
},
|
|
amount: testAmount,
|
|
expected: upperBound,
|
|
},
|
|
{
|
|
name: "some fuzz",
|
|
randIntFn: func(int64) (int64, error) {
|
|
return 123, nil
|
|
},
|
|
amount: testAmount,
|
|
expected: lowerBound + 123,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
val, err := hideAmount(
|
|
test.randIntFn,
|
|
relativeVariation,
|
|
test.amount,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.expected, val)
|
|
})
|
|
}
|
|
|
|
// Subtest with real randomness.
|
|
t.Run("real randomness for small numbers", func(t *testing.T) {
|
|
for i := 0; i < 1000; i++ {
|
|
_, err := hideAmount(
|
|
CryptoRandIntn,
|
|
relativeVariation,
|
|
uint64(i),
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
t.Run("hideAmount overflow validation", func(t *testing.T) {
|
|
// amount > math.MaxInt64 should fail.
|
|
_, err := hideAmount(
|
|
CryptoRandIntn,
|
|
relativeVariation,
|
|
math.MaxInt64+1,
|
|
)
|
|
require.Error(t, err)
|
|
|
|
// amount <= math.MaxInt64 but amount + fuzzInterval >
|
|
// math.MaxInt64 should fail.
|
|
_, err = hideAmount(
|
|
CryptoRandIntn,
|
|
0.05,
|
|
math.MaxInt64-100,
|
|
)
|
|
require.Error(t, err)
|
|
|
|
// A value that just fits should succeed.
|
|
_, err = hideAmount(
|
|
CryptoRandIntn,
|
|
0.1,
|
|
922337203685477580,
|
|
)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
// TestHideTimestamp test correct timestamp hiding.
|
|
func TestHideTimestamp(t *testing.T) {
|
|
timestamp := time.Unix(1_000_000, 0)
|
|
absoluteVariation := time.Duration(10) * time.Minute
|
|
lowerBound := timestamp.Add(-absoluteVariation)
|
|
upperBound := timestamp.Add(absoluteVariation)
|
|
|
|
tests := []struct {
|
|
name string
|
|
randIntFn func(int64) (int64, error)
|
|
timestamp time.Time
|
|
expected time.Time
|
|
}{
|
|
{
|
|
name: "zero timestamp",
|
|
randIntFn: func(int64) (int64, error) { return 0, nil },
|
|
},
|
|
{
|
|
name: "min value",
|
|
randIntFn: func(int64) (int64, error) { return 0, nil },
|
|
timestamp: timestamp,
|
|
expected: lowerBound,
|
|
},
|
|
{
|
|
name: "max value",
|
|
randIntFn: func(n int64) (int64, error) {
|
|
return int64(upperBound.Sub(lowerBound)), nil
|
|
},
|
|
timestamp: timestamp,
|
|
expected: upperBound,
|
|
},
|
|
{
|
|
name: "some fuzz",
|
|
randIntFn: func(int64) (int64, error) {
|
|
return 123, nil
|
|
},
|
|
timestamp: timestamp,
|
|
expected: lowerBound.Add(time.Duration(123)),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
val, err := hideTimestamp(
|
|
test.randIntFn,
|
|
absoluteVariation,
|
|
test.timestamp,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.expected, val)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHideBool test correct boolean hiding.
|
|
func TestHideBool(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
random int64
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "random value",
|
|
random: 100,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "exact threshold value",
|
|
random: 1,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "below threshold value",
|
|
random: 0,
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
val, err := hideBool(func(n int64) (int64, error) {
|
|
return tc.random, nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, val)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestObfuscateConfig tests that we substitute substrings in the config
|
|
// correctly.
|
|
//
|
|
//nolint:ll
|
|
func TestObfuscateConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config []byte
|
|
knownMap map[string]string
|
|
privacyFlags session.PrivacyFlags
|
|
expectedNewPairs int
|
|
expectErr bool
|
|
notExpectSameLen bool
|
|
expectUnobfuscated bool
|
|
}{
|
|
{
|
|
name: "empty",
|
|
},
|
|
{
|
|
// We substitute pubkeys of different forms.
|
|
name: "several pubkeys",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
|
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
|
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
|
expectedNewPairs: 4,
|
|
},
|
|
{
|
|
// A flag can be used to turn off obfuscation for
|
|
// pubkeys.
|
|
name: "no pubkeys obfuscation",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
|
|
`"1234567890123"]}`),
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearPubkeys,
|
|
},
|
|
expectedNewPairs: 1,
|
|
},
|
|
{
|
|
// We don't generate new pairs for pubkeys that we
|
|
// already have a mapping.
|
|
name: "several pubkeys with known replacement or duplicates",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11cfdc7a8ec5c9d495270de66fdbf",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
|
`"DEAD2708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85",` +
|
|
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
|
knownMap: map[string]string{
|
|
"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4": "123456789012345678901234567890123456789012345678901234567890123456",
|
|
},
|
|
expectedNewPairs: 3,
|
|
},
|
|
{
|
|
// We don't obfuscate if we already have a mapping, but
|
|
// the obfuscation is turned off.
|
|
name: "several pubkeys with known replacement or duplicates",
|
|
config: []byte(`{"list":` +
|
|
`["586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85"]}`),
|
|
knownMap: map[string]string{
|
|
"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4": "123456789012345678901234567890123456789012345678901234567890123456",
|
|
},
|
|
privacyFlags: []session.PrivacyFlag{session.ClearPubkeys},
|
|
expectedNewPairs: 0,
|
|
expectUnobfuscated: true,
|
|
},
|
|
{
|
|
// We don't substitute unknown items.
|
|
name: "all invalid pubkeys",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["d23da57575cdcb878ac191e1e0c8a5c4f061b11",` +
|
|
`"586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4dead",` +
|
|
`"x86b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"]}`),
|
|
expectedNewPairs: 0,
|
|
},
|
|
{
|
|
// We only substitute channel ids that have a sane
|
|
// format.
|
|
name: "channel ids",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["1",` +
|
|
`"12345",` +
|
|
`"1234567890123",` +
|
|
`"1234567890123456789",` +
|
|
`"123456789012345678901"]}`),
|
|
expectedNewPairs: 2,
|
|
},
|
|
{
|
|
// We obfuscate channel points, the character length may
|
|
// vary due to the output index.
|
|
name: "channel points",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
|
|
`"e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:1",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca3:1",` +
|
|
`"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca:3000"]}`),
|
|
expectedNewPairs: 2,
|
|
notExpectSameLen: true,
|
|
},
|
|
{
|
|
// We only act on items that are in lists of strings.
|
|
name: "single pubkey with another field",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["586b59212da4623c40dcc68c4573da1719e5893630790c9f2db8940fff3efd8cd4"],` +
|
|
`"another":"0e092708c9e737115ff14a85b65466561280d77c1b8cd666bc655536ad81ccca85"}`),
|
|
expectedNewPairs: 1,
|
|
},
|
|
{
|
|
// We don't obfuscate any numbers even though they may
|
|
// be channel ids. This is to be able to set numerical
|
|
// values in the range of channel ids.
|
|
name: "number",
|
|
config: []byte(`{"version":1,"number":12345678901234567890}`),
|
|
expectedNewPairs: 0,
|
|
},
|
|
{
|
|
// We don't allow to mix strings with other types, which
|
|
// may be a configuration mistake.
|
|
name: "list of invalid types",
|
|
config: []byte(`{"version":1,"channels":` +
|
|
`[12345,` +
|
|
`"e092708c9e737115ff14a85ab65466561280d77c1b8cd666bc655536ad81ccca:1"]}`),
|
|
expectErr: true,
|
|
expectedNewPairs: 0,
|
|
},
|
|
{
|
|
// A list of numbers is not obfuscated. Those can be
|
|
// useful to submit histograms for example.
|
|
name: "channel ids",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`[1,` +
|
|
`12345,` +
|
|
`1234567890123,` +
|
|
`1234567890123456789,` +
|
|
`123456789012345678901]}`),
|
|
expectedNewPairs: 0,
|
|
},
|
|
{
|
|
// We don't obfuscate channel ids and points if the
|
|
// corresponding privacy flag is set.
|
|
// format.
|
|
name: "clear channel ids",
|
|
config: []byte(`{"version":1,"list":` +
|
|
`["1234567890123",` +
|
|
`"e092708c9e737115ff14a85ab65466561280d77c1b8cd666bc655536ad81ccca:1"]}`),
|
|
expectedNewPairs: 0,
|
|
privacyFlags: []session.PrivacyFlag{
|
|
session.ClearChanIDs,
|
|
},
|
|
},
|
|
}
|
|
|
|
// assertConfigStructure checks that the structure of the config is
|
|
// preserved.
|
|
assertConfigStructure := func(wantConfig, gotConfig []byte) {
|
|
t.Helper()
|
|
|
|
if len(wantConfig) == 0 {
|
|
require.Equal(t, wantConfig, gotConfig)
|
|
|
|
return
|
|
}
|
|
|
|
var wantConfigMap map[string]any
|
|
err := json.Unmarshal(wantConfig, &wantConfigMap)
|
|
require.NoError(t, err)
|
|
|
|
var gotConfigMap map[string]any
|
|
err = json.Unmarshal(gotConfig, &gotConfigMap)
|
|
require.NoError(t, err)
|
|
|
|
// We test that the number of top level items is the same.
|
|
require.Equal(t, len(wantConfigMap), len(gotConfigMap))
|
|
|
|
listLen := func(config map[string]any) int {
|
|
for k, v := range config {
|
|
if k == "list" {
|
|
list, ok := v.([]interface{})
|
|
require.True(t, ok)
|
|
return len(list)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// We test that we have the same number of items in the list.
|
|
require.Equal(t, listLen(wantConfigMap), listLen(gotConfigMap))
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := firewalldb.NewPrivacyMapPairs(tt.knownMap)
|
|
|
|
config, privMapPairs, err := ObfuscateConfig(
|
|
db, tt.config, tt.privacyFlags,
|
|
)
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
// We expect the config to be obfuscated in any parts
|
|
// only if there is sensitive data.
|
|
if tt.expectedNewPairs > 0 {
|
|
require.NotEqual(t, config, tt.config)
|
|
}
|
|
|
|
// We check that we recognized the correct number of new
|
|
// substitutions.
|
|
require.Equal(t, tt.expectedNewPairs,
|
|
len(privMapPairs))
|
|
|
|
// We expect the same number of items in the config
|
|
// after obfuscation.
|
|
assertConfigStructure(tt.config, config)
|
|
|
|
// We don't perform exact length checks for cases where
|
|
// we know the length can change.
|
|
if !tt.notExpectSameLen {
|
|
require.Equal(t, len(tt.config), len(config))
|
|
}
|
|
|
|
// We expect the config to be unobfuscated if we have
|
|
// the corresponding privacy flag.
|
|
if tt.expectUnobfuscated {
|
|
require.Equal(t, tt.config, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// mean computes the mean of the given slice of numbers.
|
|
func mean(numbers []uint64) uint64 {
|
|
sum := uint64(0)
|
|
|
|
for _, n := range numbers {
|
|
sum += n
|
|
}
|
|
|
|
return sum / uint64(len(numbers))
|
|
}
|
|
|
|
// variance computes the variance of the given slice of numbers.
|
|
func variance(numbers []uint64) uint64 {
|
|
mean := mean(numbers)
|
|
sum := 0.0
|
|
|
|
// We divide in each step to have smaller numbers.
|
|
norm := float64(len(numbers) - 1)
|
|
|
|
for _, n := range numbers {
|
|
sum += float64((n-mean)*(n-mean)) / norm
|
|
}
|
|
|
|
return uint64(sum)
|
|
}
|
|
|
|
// Test32BitOverflowAndTruncation ensures values > MaxInt32 do not truncate on
|
|
// 32-bit runtimes.
|
|
func Test32BitOverflowAndTruncation(t *testing.T) {
|
|
largeValue := int64(math.MaxInt32) + 1
|
|
|
|
// Ensure randBetween successfully operates on values larger than
|
|
// MaxInt32 without any architecture-dependent truncation.
|
|
mockRand := func(n int64) (int64, error) {
|
|
return n - 1, nil
|
|
}
|
|
|
|
val, err := randBetween(mockRand, largeValue, largeValue+10)
|
|
require.NoError(t, err)
|
|
require.Equal(t, largeValue+9, val)
|
|
|
|
// Ensure hideTimestamp works with a 64-bit nanosecond timestamp
|
|
// representing a real far-future date (> MaxInt32).
|
|
farFutureTime := time.Unix(0, 1600000000000000000) // ~Year 2020 in ns
|
|
variation := 10 * time.Minute
|
|
resTime, err := hideTimestamp(mockRand, variation, farFutureTime)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, resTime)
|
|
}
|