lightning-terminal/firewall/privacy_mapper_test.go
bitromortac 6d89f07037 firewall: fix 32-bit int truncation
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.
2026-07-21 12:04:33 +00:00

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)
}