From 64fc92abe795b9e2aeca1ad8892258291edfe2bf Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 12:22:13 +0200 Subject: [PATCH 1/6] graph/db: update nextUpdateTime in tests Let it return time.Time instead. Also, make use of the helper in a few more places. --- graph/db/graph_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 02e96dfc9..02941fb19 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -77,7 +77,7 @@ func createLightningNode(priv *btcec.PrivateKey) *models.LightningNode { n := &models.LightningNode{ HaveNodeAnnouncement: true, AuthSigBytes: testSig.Serialize(), - LastUpdate: time.Unix(nextUpdateTime(), 0), + LastUpdate: nextUpdateTime(), Color: color.RGBA{1, 2, 3, 0}, Alias: "kek" + hex.EncodeToString(pub), Features: testFeatures, @@ -784,7 +784,7 @@ func createChannelEdge(node1, node2 *models.LightningNode, edge1 := &models.ChannelEdgePolicy{ SigBytes: testSig.Serialize(), ChannelID: chanID, - LastUpdate: time.Unix(433453, 0), + LastUpdate: nextUpdateTime(), MessageFlags: 1, ChannelFlags: 0, TimeLockDelta: 99, @@ -798,7 +798,7 @@ func createChannelEdge(node1, node2 *models.LightningNode, edge2 := &models.ChannelEdgePolicy{ SigBytes: testSig.Serialize(), ChannelID: chanID, - LastUpdate: time.Unix(124234, 0), + LastUpdate: nextUpdateTime(), MessageFlags: 1, ChannelFlags: 1, TimeLockDelta: 99, @@ -3455,13 +3455,13 @@ var ( updateTimeMu sync.Mutex ) -func nextUpdateTime() int64 { +func nextUpdateTime() time.Time { updateTimeMu.Lock() defer updateTimeMu.Unlock() updateTime++ - return updateTime + return time.Unix(updateTime, 0) } // TestNodeIsPublic ensures that we properly detect nodes that are seen as @@ -3506,7 +3506,7 @@ func TestNodeIsPublic(t *testing.T) { graphs := []*ChannelGraph{aliceGraph, bobGraph, carolGraph} for _, graph := range graphs { for _, node := range nodes { - node.LastUpdate = time.Unix(nextUpdateTime(), 0) + node.LastUpdate = nextUpdateTime() err := graph.AddLightningNode(ctx, node) require.NoError(t, err) } @@ -3621,7 +3621,7 @@ func TestDisabledChannelIDs(t *testing.T) { // Adding a new channel edge to the graph. edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) - node2.LastUpdate = time.Unix(nextUpdateTime(), 0) + node2.LastUpdate = nextUpdateTime() if err := graph.AddLightningNode(ctx, node2); err != nil { t.Fatalf("unable to add node: %v", err) } From fa32f97c05f1cfd0d46d8f3deac4f8f8638598ca Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 12:27:15 +0200 Subject: [PATCH 2/6] graph/db: unit test to demonstrate SQL chan policies bug This commit adds a new graph store unit test that demonstrates that there is currently a bug in the SQL version of the graph store where unknown message or channel flags in a channel_update are not persisted correctly. This will be fixed in an upcoming commit. --- graph/db/graph_test.go | 88 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index 02941fb19..f9d846823 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -908,6 +908,92 @@ func TestEdgeInfoUpdates(t *testing.T) { assertEdgeInfoEqual(t, dbEdgeInfo, edgeInfo) } +// TestEdgePolicyCRUD tests basic CRUD operations for edge policies. +// +// NOTE: this currently demonstrates a bug in the SQL backend where +// Channel Flags and Message Flags are not properly stored. This will be fixed +// in an upcoming commit. +func TestEdgePolicyCRUD(t *testing.T) { + t.Parallel() + ctx := context.Background() + + graph := MakeTestGraph(t) + + node1 := createTestVertex(t) + node2 := createTestVertex(t) + + // Create an edge. Don't add it to the DB yet. + edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) + + updateAndAssertPolicies := func(expErr bool) { + // Make copies of the policies before calling UpdateEdgePolicy + // to avoid any data race's that can occur during async calls + // that UpdateEdgePolicy may trigger. + edge1 := copyEdgePolicy(edge1) + edge2 := copyEdgePolicy(edge2) + + edge1.LastUpdate = nextUpdateTime() + edge2.LastUpdate = nextUpdateTime() + + require.NoError(t, graph.UpdateEdgePolicy(ctx, edge1)) + require.NoError(t, graph.UpdateEdgePolicy(ctx, edge2)) + + // Use the ForEachChannel method to fetch the policies and + // assert that the deserialized policies match the original + // ones. + err := graph.ForEachChannel(func(info *models.ChannelEdgeInfo, + policy1 *models.ChannelEdgePolicy, + policy2 *models.ChannelEdgePolicy) error { + + if expErr { + require.Error( + t, compareEdgePolicies(edge1, policy1), + ) + require.Error( + t, compareEdgePolicies(edge2, policy2), + ) + + return nil + } + + require.NoError(t, compareEdgePolicies(edge1, policy1)) + require.NoError(t, compareEdgePolicies(edge2, policy2)) + + return nil + }) + require.NoError(t, err) + } + + // Make sure inserting the policy at this point, before the edge info + // is added, will fail. + require.ErrorIs(t, graph.UpdateEdgePolicy(ctx, edge1), ErrEdgeNotFound) + + // Now add the edge. + require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) + + updateAndAssertPolicies(false) + + // Update one of the edges to have no extra opaque data. + edge1.ExtraOpaqueData = nil + + updateAndAssertPolicies(false) + + // Update one of the edges to have ChannelFlags include a bit unknown + // to us. + edge1.ChannelFlags |= 1 << 6 + + // Update the other edge to have MessageFlags include a bit unknown to + // us. + edge2.MessageFlags |= 1 << 4 + + // NOTE: If the backend is SQL, then we expect an error here as + // there is currently a bug in the SQL backend where + // ChannelFlags and MessageFlags are not properly stored. This will + // be fixed in an upcoming commit. + _, isSQLImp := graph.V1Store.(*SQLStore) + updateAndAssertPolicies(isSQLImp) +} + func assertNodeInCache(t *testing.T, g *ChannelGraph, n *models.LightningNode, expectedFeatures *lnwire.FeatureVector) { @@ -4114,7 +4200,7 @@ func TestBatchedUpdateEdgePolicy(t *testing.T) { // Make sure inserting the policy at this point, before the edge info // is added, will fail. - require.Error(t, ErrEdgeNotFound, graph.UpdateEdgePolicy(ctx, edge1)) + require.ErrorIs(t, graph.UpdateEdgePolicy(ctx, edge1), ErrEdgeNotFound) // Add the edge info. require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) From 90aacaae2a2d5b8462c9c5f6c6b619f5bf7b5d90 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 12:28:55 +0200 Subject: [PATCH 3/6] sqldb: add SQLInt16 helper --- sqldb/sqlutils.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sqldb/sqlutils.go b/sqldb/sqlutils.go index 1bf9164e2..bafba8416 100644 --- a/sqldb/sqlutils.go +++ b/sqldb/sqlutils.go @@ -11,6 +11,18 @@ import ( // reset function ExecTx calls. var NoOpReset = func() {} +// SQLInt16 turns a numerical integer type into the NullInt16 that sql/sqlc +// uses when an integer field can be permitted to be NULL. +// +// We use this constraints.Integer constraint here which maps to all signed and +// unsigned integer types. +func SQLInt16[T constraints.Integer](num T) sql.NullInt16 { + return sql.NullInt16{ + Int16: int16(num), + Valid: true, + } +} + // SQLInt32 turns a numerical integer type into the NullInt32 that sql/sqlc // uses when an integer field can be permitted to be NULL. // From 4a05e5a226bd700c797d917aaeb32ba6979760d7 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 11:27:34 +0200 Subject: [PATCH 4/6] sqldb/sqlc: add message and channe flags to channel_policies table We need to explicitly store the entire bitfield types since we may have channel_updates with bitfields containing bits we just dont need or understand but we still need to store the entire bitfield so that the reconstructed announcement remains valid. This commit only adds the new columns but does not use them yet. NOTE: this is ok since the migration adding this schema is not available in the production build yet. --- sqldb/sqlc/graph.sql.go | 75 ++++++++++++++++++++++- sqldb/sqlc/migrations/000007_graph.up.sql | 21 ++++++- sqldb/sqlc/models.go | 2 + sqldb/sqlc/queries/graph.sql | 27 +++++++- 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/sqldb/sqlc/graph.sql.go b/sqldb/sqlc/graph.sql.go index 0387f612e..639696d0f 100644 --- a/sqldb/sqlc/graph.sql.go +++ b/sqldb/sqlc/graph.sql.go @@ -377,6 +377,8 @@ SELECT cp1.disabled AS policy_1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy_1_message_flags, + cp1.channel_flags AS policy_1_channel_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -392,6 +394,8 @@ SELECT cp2.disabled AS policy_2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy_2_message_flags, + cp2.channel_flags AS policy_2_channel_flags, cp2.signature AS policy_2_signature FROM channels c JOIN nodes n1 ON c.node_id_1 = n1.id @@ -424,6 +428,8 @@ type GetChannelByOutpointWithPoliciesRow struct { Policy1Disabled sql.NullBool Policy1InboundBaseFeeMsat sql.NullInt64 Policy1InboundFeeRateMilliMsat sql.NullInt64 + Policy1MessageFlags sql.NullInt16 + Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -437,6 +443,8 @@ type GetChannelByOutpointWithPoliciesRow struct { Policy2Disabled sql.NullBool Policy2InboundBaseFeeMsat sql.NullInt64 Policy2InboundFeeRateMilliMsat sql.NullInt64 + Policy2MessageFlags sql.NullInt16 + Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte } @@ -471,6 +479,8 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC &i.Policy1Disabled, &i.Policy1InboundBaseFeeMsat, &i.Policy1InboundFeeRateMilliMsat, + &i.Policy1MessageFlags, + &i.Policy1ChannelFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -484,6 +494,8 @@ func (q *Queries) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetC &i.Policy2Disabled, &i.Policy2InboundBaseFeeMsat, &i.Policy2InboundFeeRateMilliMsat, + &i.Policy2MessageFlags, + &i.Policy2ChannelFlags, &i.Policy2Signature, ) return i, err @@ -539,6 +551,8 @@ SELECT cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 @@ -554,6 +568,8 @@ SELECT cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy_2_message_flags, + cp2.channel_flags AS policy_2_channel_flags, cp2.signature AS policy2_signature FROM channels c @@ -588,6 +604,8 @@ type GetChannelBySCIDWithPoliciesRow struct { Policy1Disabled sql.NullBool Policy1InboundBaseFeeMsat sql.NullInt64 Policy1InboundFeeRateMilliMsat sql.NullInt64 + Policy1MessageFlags sql.NullInt16 + Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -601,6 +619,8 @@ type GetChannelBySCIDWithPoliciesRow struct { Policy2Disabled sql.NullBool Policy2InboundBaseFeeMsat sql.NullInt64 Policy2InboundFeeRateMilliMsat sql.NullInt64 + Policy2MessageFlags sql.NullInt16 + Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte } @@ -647,6 +667,8 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.Policy1Disabled, &i.Policy1InboundBaseFeeMsat, &i.Policy1InboundFeeRateMilliMsat, + &i.Policy1MessageFlags, + &i.Policy1ChannelFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -660,6 +682,8 @@ func (q *Queries) GetChannelBySCIDWithPolicies(ctx context.Context, arg GetChann &i.Policy2Disabled, &i.Policy2InboundBaseFeeMsat, &i.Policy2InboundFeeRateMilliMsat, + &i.Policy2MessageFlags, + &i.Policy2ChannelFlags, &i.Policy2Signature, ) return i, err @@ -725,7 +749,7 @@ func (q *Queries) GetChannelFeaturesAndExtras(ctx context.Context, channelID int } const getChannelPolicyByChannelAndNode = `-- name: GetChannelPolicyByChannelAndNode :one -SELECT id, version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, max_htlc_msat, last_update, disabled, inbound_base_fee_msat, inbound_fee_rate_milli_msat, signature +SELECT id, version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, max_htlc_msat, last_update, disabled, inbound_base_fee_msat, inbound_fee_rate_milli_msat, message_flags, channel_flags, signature FROM channel_policies WHERE channel_id = $1 AND node_id = $2 @@ -755,6 +779,8 @@ func (q *Queries) GetChannelPolicyByChannelAndNode(ctx context.Context, arg GetC &i.Disabled, &i.InboundBaseFeeMsat, &i.InboundFeeRateMilliMsat, + &i.MessageFlags, + &i.ChannelFlags, &i.Signature, ) return i, err @@ -834,6 +860,8 @@ SELECT cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 (node_id_2) @@ -849,6 +877,8 @@ SELECT cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy2_signature FROM channels c @@ -894,6 +924,8 @@ type GetChannelsByPolicyLastUpdateRangeRow struct { Policy1Disabled sql.NullBool Policy1InboundBaseFeeMsat sql.NullInt64 Policy1InboundFeeRateMilliMsat sql.NullInt64 + Policy1MessageFlags sql.NullInt16 + Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -907,6 +939,8 @@ type GetChannelsByPolicyLastUpdateRangeRow struct { Policy2Disabled sql.NullBool Policy2InboundBaseFeeMsat sql.NullInt64 Policy2InboundFeeRateMilliMsat sql.NullInt64 + Policy2MessageFlags sql.NullInt16 + Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte } @@ -959,6 +993,8 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.Policy1Disabled, &i.Policy1InboundBaseFeeMsat, &i.Policy1InboundFeeRateMilliMsat, + &i.Policy1MessageFlags, + &i.Policy1ChannelFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -972,6 +1008,8 @@ func (q *Queries) GetChannelsByPolicyLastUpdateRange(ctx context.Context, arg Ge &i.Policy2Disabled, &i.Policy2InboundBaseFeeMsat, &i.Policy2InboundFeeRateMilliMsat, + &i.Policy2MessageFlags, + &i.Policy2ChannelFlags, &i.Policy2Signature, ); err != nil { return nil, err @@ -1710,6 +1748,8 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 @@ -1725,6 +1765,8 @@ SELECT c.id, c.version, c.scid, c.node_id_1, c.node_id_2, c.outpoint, c.capacity cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy2_signature FROM channels c @@ -1759,6 +1801,8 @@ type ListChannelsByNodeIDRow struct { Policy1Disabled sql.NullBool Policy1InboundBaseFeeMsat sql.NullInt64 Policy1InboundFeeRateMilliMsat sql.NullInt64 + Policy1MessageFlags sql.NullInt16 + Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -1772,6 +1816,8 @@ type ListChannelsByNodeIDRow struct { Policy2Disabled sql.NullBool Policy2InboundBaseFeeMsat sql.NullInt64 Policy2InboundFeeRateMilliMsat sql.NullInt64 + Policy2MessageFlags sql.NullInt16 + Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte } @@ -1812,6 +1858,8 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo &i.Policy1Disabled, &i.Policy1InboundBaseFeeMsat, &i.Policy1InboundFeeRateMilliMsat, + &i.Policy1MessageFlags, + &i.Policy1ChannelFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -1825,6 +1873,8 @@ func (q *Queries) ListChannelsByNodeID(ctx context.Context, arg ListChannelsByNo &i.Policy2Disabled, &i.Policy2InboundBaseFeeMsat, &i.Policy2InboundFeeRateMilliMsat, + &i.Policy2MessageFlags, + &i.Policy2ChannelFlags, &i.Policy2Signature, ); err != nil { return nil, err @@ -1910,6 +1960,8 @@ SELECT cp1.disabled AS policy_1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -1925,6 +1977,8 @@ SELECT cp2.disabled AS policy_2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy_2_signature FROM channels c @@ -1961,6 +2015,8 @@ type ListChannelsWithPoliciesPaginatedRow struct { Policy1Disabled sql.NullBool Policy1InboundBaseFeeMsat sql.NullInt64 Policy1InboundFeeRateMilliMsat sql.NullInt64 + Policy1MessageFlags sql.NullInt16 + Policy1ChannelFlags sql.NullInt16 Policy1Signature []byte Policy2ID sql.NullInt64 Policy2NodeID sql.NullInt64 @@ -1974,6 +2030,8 @@ type ListChannelsWithPoliciesPaginatedRow struct { Policy2Disabled sql.NullBool Policy2InboundBaseFeeMsat sql.NullInt64 Policy2InboundFeeRateMilliMsat sql.NullInt64 + Policy2MessageFlags sql.NullInt16 + Policy2ChannelFlags sql.NullInt16 Policy2Signature []byte } @@ -2014,6 +2072,8 @@ func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg Lis &i.Policy1Disabled, &i.Policy1InboundBaseFeeMsat, &i.Policy1InboundFeeRateMilliMsat, + &i.Policy1MessageFlags, + &i.Policy1ChannelFlags, &i.Policy1Signature, &i.Policy2ID, &i.Policy2NodeID, @@ -2027,6 +2087,8 @@ func (q *Queries) ListChannelsWithPoliciesPaginated(ctx context.Context, arg Lis &i.Policy2Disabled, &i.Policy2InboundBaseFeeMsat, &i.Policy2InboundFeeRateMilliMsat, + &i.Policy2MessageFlags, + &i.Policy2ChannelFlags, &i.Policy2Signature, ); err != nil { return nil, err @@ -2139,9 +2201,10 @@ INSERT INTO channel_policies ( version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, last_update, disabled, max_htlc_msat, inbound_base_fee_msat, - inbound_fee_rate_milli_msat, signature + inbound_fee_rate_milli_msat, message_flags, channel_flags, + signature ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) ON CONFLICT (channel_id, node_id, version) -- Update the following fields if a conflict occurs on channel_id, @@ -2156,6 +2219,8 @@ ON CONFLICT (channel_id, node_id, version) max_htlc_msat = EXCLUDED.max_htlc_msat, inbound_base_fee_msat = EXCLUDED.inbound_base_fee_msat, inbound_fee_rate_milli_msat = EXCLUDED.inbound_fee_rate_milli_msat, + message_flags = EXCLUDED.message_flags, + channel_flags = EXCLUDED.channel_flags, signature = EXCLUDED.signature WHERE EXCLUDED.last_update > channel_policies.last_update RETURNING id @@ -2174,6 +2239,8 @@ type UpsertEdgePolicyParams struct { MaxHtlcMsat sql.NullInt64 InboundBaseFeeMsat sql.NullInt64 InboundFeeRateMilliMsat sql.NullInt64 + MessageFlags sql.NullInt16 + ChannelFlags sql.NullInt16 Signature []byte } @@ -2191,6 +2258,8 @@ func (q *Queries) UpsertEdgePolicy(ctx context.Context, arg UpsertEdgePolicyPara arg.MaxHtlcMsat, arg.InboundBaseFeeMsat, arg.InboundFeeRateMilliMsat, + arg.MessageFlags, + arg.ChannelFlags, arg.Signature, ) var id int64 diff --git a/sqldb/sqlc/migrations/000007_graph.up.sql b/sqldb/sqlc/migrations/000007_graph.up.sql index c56478ec5..2217aee9e 100644 --- a/sqldb/sqlc/migrations/000007_graph.up.sql +++ b/sqldb/sqlc/migrations/000007_graph.up.sql @@ -228,7 +228,7 @@ CREATE TABLE IF NOT EXISTS channel_policies ( -- The DB ID of the node that created the policy update. node_id BIGINT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, - -- The number of blocks that the node will subtrace from the expiry + -- The number of blocks that the node will subtract from the expiry -- of an incoming HTLC. timelock INTEGER NOT NULL, @@ -266,6 +266,25 @@ CREATE TABLE IF NOT EXISTS channel_policies ( -- charge for incoming HTLCs. inbound_fee_rate_milli_msat BIGINT, + -- A bitfield used to provide extra details about the message. This is + -- nullable since for later protocol versions, this might not be + -- present. Even though we explicitly store some details from the + -- message_flags in other ways (for example, max_htlc_msat being not + -- null means that bit 0 of the message_flags is set), we still store + -- the full message_flags field so that we can reconstruct the original + -- announcement if needed, even if the bitfield contains bits that we + -- don't use or understand. + message_flags SMALLINT CHECK (message_flags >= 0 AND message_flags <= 255), + + -- A bitfield used to provide extra details about the update. This is + -- nullable since for later protocol versions, this might not be present. + -- Even though we explicitly store some details from the channel_flags in + -- other ways (for example, the disabled field's value is derived directly + -- from the channel_flags), we still store the full bitfield so that we + -- can reconstruct the original announcement if needed, even if the + -- bitfield contains bits that we don't use or understand. + channel_flags SMALLINT CHECK (channel_flags >= 0 AND channel_flags <= 255), + -- The signature of the channel update announcement. signature BLOB ); diff --git a/sqldb/sqlc/models.go b/sqldb/sqlc/models.go index 887870b1c..ca43bed1f 100644 --- a/sqldb/sqlc/models.go +++ b/sqldb/sqlc/models.go @@ -69,6 +69,8 @@ type ChannelPolicy struct { Disabled sql.NullBool InboundBaseFeeMsat sql.NullInt64 InboundFeeRateMilliMsat sql.NullInt64 + MessageFlags sql.NullInt16 + ChannelFlags sql.NullInt16 Signature []byte } diff --git a/sqldb/sqlc/queries/graph.sql b/sqldb/sqlc/queries/graph.sql index 681f80def..dfa32ef82 100644 --- a/sqldb/sqlc/queries/graph.sql +++ b/sqldb/sqlc/queries/graph.sql @@ -296,6 +296,8 @@ SELECT cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 (node_id_2) @@ -311,6 +313,8 @@ SELECT cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy2_signature FROM channels c @@ -353,6 +357,8 @@ SELECT cp1.disabled AS policy_1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy_1_message_flags, + cp1.channel_flags AS policy_1_channel_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -368,6 +374,8 @@ SELECT cp2.disabled AS policy_2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy_2_message_flags, + cp2.channel_flags AS policy_2_channel_flags, cp2.signature AS policy_2_signature FROM channels c JOIN nodes n1 ON c.node_id_1 = n1.id @@ -406,6 +414,8 @@ SELECT sqlc.embed(c), cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 @@ -421,6 +431,8 @@ SELECT sqlc.embed(c), cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy2_signature FROM channels c @@ -468,6 +480,8 @@ SELECT cp1.disabled AS policy_1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy_1_signature, -- Node 2 policy @@ -483,6 +497,8 @@ SELECT cp2.disabled AS policy_2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy2_message_flags, + cp2.channel_flags AS policy2_channel_flags, cp2.signature AS policy_2_signature FROM channels c @@ -532,9 +548,10 @@ INSERT INTO channel_policies ( version, channel_id, node_id, timelock, fee_ppm, base_fee_msat, min_htlc_msat, last_update, disabled, max_htlc_msat, inbound_base_fee_msat, - inbound_fee_rate_milli_msat, signature + inbound_fee_rate_milli_msat, message_flags, channel_flags, + signature ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) ON CONFLICT (channel_id, node_id, version) -- Update the following fields if a conflict occurs on channel_id, @@ -549,6 +566,8 @@ ON CONFLICT (channel_id, node_id, version) max_htlc_msat = EXCLUDED.max_htlc_msat, inbound_base_fee_msat = EXCLUDED.inbound_base_fee_msat, inbound_fee_rate_milli_msat = EXCLUDED.inbound_fee_rate_milli_msat, + message_flags = EXCLUDED.message_flags, + channel_flags = EXCLUDED.channel_flags, signature = EXCLUDED.signature WHERE EXCLUDED.last_update > channel_policies.last_update RETURNING id; @@ -579,6 +598,8 @@ SELECT cp1.disabled AS policy1_disabled, cp1.inbound_base_fee_msat AS policy1_inbound_base_fee_msat, cp1.inbound_fee_rate_milli_msat AS policy1_inbound_fee_rate_milli_msat, + cp1.message_flags AS policy1_message_flags, + cp1.channel_flags AS policy1_channel_flags, cp1.signature AS policy1_signature, -- Policy 2 @@ -594,6 +615,8 @@ SELECT cp2.disabled AS policy2_disabled, cp2.inbound_base_fee_msat AS policy2_inbound_base_fee_msat, cp2.inbound_fee_rate_milli_msat AS policy2_inbound_fee_rate_milli_msat, + cp2.message_flags AS policy_2_message_flags, + cp2.channel_flags AS policy_2_channel_flags, cp2.signature AS policy2_signature FROM channels c From e4137a3f1496644eff61df0f558383055e8f6afa Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 12:32:25 +0200 Subject: [PATCH 5/6] graph/db: fix ChanUpdate message/channel flag bug Here we start using the newly added message_flags and channel_flags columns of the channel_policies table. The test added previoulsy to demonstrate the bug is now updated to show that the bug has been fixed. --- graph/db/graph_test.go | 28 +++----------------- graph/db/sql_store.go | 59 ++++++++++++++++++++++++++---------------- sqldb/sqlutils.go | 7 +++++ 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index f9d846823..5aec3c2e4 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -909,10 +909,6 @@ func TestEdgeInfoUpdates(t *testing.T) { } // TestEdgePolicyCRUD tests basic CRUD operations for edge policies. -// -// NOTE: this currently demonstrates a bug in the SQL backend where -// Channel Flags and Message Flags are not properly stored. This will be fixed -// in an upcoming commit. func TestEdgePolicyCRUD(t *testing.T) { t.Parallel() ctx := context.Background() @@ -925,7 +921,7 @@ func TestEdgePolicyCRUD(t *testing.T) { // Create an edge. Don't add it to the DB yet. edgeInfo, edge1, edge2 := createChannelEdge(node1, node2) - updateAndAssertPolicies := func(expErr bool) { + updateAndAssertPolicies := func() { // Make copies of the policies before calling UpdateEdgePolicy // to avoid any data race's that can occur during async calls // that UpdateEdgePolicy may trigger. @@ -945,17 +941,6 @@ func TestEdgePolicyCRUD(t *testing.T) { policy1 *models.ChannelEdgePolicy, policy2 *models.ChannelEdgePolicy) error { - if expErr { - require.Error( - t, compareEdgePolicies(edge1, policy1), - ) - require.Error( - t, compareEdgePolicies(edge2, policy2), - ) - - return nil - } - require.NoError(t, compareEdgePolicies(edge1, policy1)) require.NoError(t, compareEdgePolicies(edge2, policy2)) @@ -971,12 +956,12 @@ func TestEdgePolicyCRUD(t *testing.T) { // Now add the edge. require.NoError(t, graph.AddChannelEdge(ctx, edgeInfo)) - updateAndAssertPolicies(false) + updateAndAssertPolicies() // Update one of the edges to have no extra opaque data. edge1.ExtraOpaqueData = nil - updateAndAssertPolicies(false) + updateAndAssertPolicies() // Update one of the edges to have ChannelFlags include a bit unknown // to us. @@ -986,12 +971,7 @@ func TestEdgePolicyCRUD(t *testing.T) { // us. edge2.MessageFlags |= 1 << 4 - // NOTE: If the backend is SQL, then we expect an error here as - // there is currently a bug in the SQL backend where - // ChannelFlags and MessageFlags are not properly stored. This will - // be fixed in an upcoming commit. - _, isSQLImp := graph.V1Store.(*SQLStore) - updateAndAssertPolicies(isSQLImp) + updateAndAssertPolicies() } func assertNodeInCache(t *testing.T, g *ChannelGraph, n *models.LightningNode, diff --git a/graph/db/sql_store.go b/graph/db/sql_store.go index 5ff8b7a1d..8f3a48b85 100644 --- a/graph/db/sql_store.go +++ b/graph/db/sql_store.go @@ -1271,7 +1271,7 @@ func (s *SQLStore) ForEachChannelCacheable(cb func(*models.CachedEdgeInfo, var pol1, pol2 *models.CachedEdgePolicy if dbPol1 != nil { policy1, err := buildChanPolicy( - *dbPol1, edge.ChannelID, nil, node2, true, + *dbPol1, edge.ChannelID, nil, node2, ) if err != nil { return err @@ -1281,7 +1281,7 @@ func (s *SQLStore) ForEachChannelCacheable(cb func(*models.CachedEdgeInfo, } if dbPol2 != nil { policy2, err := buildChanPolicy( - *dbPol2, edge.ChannelID, nil, node1, false, + *dbPol2, edge.ChannelID, nil, node1, ) if err != nil { return err @@ -2900,7 +2900,7 @@ func forEachNodeDirectedChannel(ctx context.Context, db SQLQueries, var p1, p2 *models.CachedEdgePolicy if dbPol1 != nil { policy1, err := buildChanPolicy( - *dbPol1, edge.ChannelID, nil, node2, true, + *dbPol1, edge.ChannelID, nil, node2, ) if err != nil { return err @@ -2910,7 +2910,7 @@ func forEachNodeDirectedChannel(ctx context.Context, db SQLQueries, } if dbPol2 != nil { policy2, err := buildChanPolicy( - *dbPol2, edge.ChannelID, nil, node1, false, + *dbPol2, edge.ChannelID, nil, node1, ) if err != nil { return err @@ -3138,6 +3138,8 @@ func updateChanEdgePolicy(ctx context.Context, tx SQLQueries, Valid: edge.MessageFlags.HasMaxHtlc(), Int64: int64(edge.MaxHTLC), }, + MessageFlags: sqldb.SQLInt16(edge.MessageFlags), + ChannelFlags: sqldb.SQLInt16(edge.ChannelFlags), InboundBaseFeeMsat: inboundBase, InboundFeeRateMilliMsat: inboundRate, Signature: edge.SigBytes, @@ -4135,7 +4137,7 @@ func getAndBuildChanPolicies(ctx context.Context, db SQLQueries, var pol1, pol2 *models.ChannelEdgePolicy if dbPol1 != nil { pol1, err = buildChanPolicy( - *dbPol1, channelID, dbPol1Extras, node2, true, + *dbPol1, channelID, dbPol1Extras, node2, ) if err != nil { return nil, nil, err @@ -4143,7 +4145,7 @@ func getAndBuildChanPolicies(ctx context.Context, db SQLQueries, } if dbPol2 != nil { pol2, err = buildChanPolicy( - *dbPol2, channelID, dbPol2Extras, node1, false, + *dbPol2, channelID, dbPol2Extras, node1, ) if err != nil { return nil, nil, err @@ -4156,8 +4158,8 @@ func getAndBuildChanPolicies(ctx context.Context, db SQLQueries, // buildChanPolicy builds a models.ChannelEdgePolicy instance from the // provided sqlc.ChannelPolicy and other required information. func buildChanPolicy(dbPolicy sqlc.ChannelPolicy, channelID uint64, - extras map[uint64][]byte, toNode route.Vertex, - isNode1 bool) (*models.ChannelEdgePolicy, error) { + extras map[uint64][]byte, + toNode route.Vertex) (*models.ChannelEdgePolicy, error) { recs, err := lnwire.CustomRecords(extras).Serialize() if err != nil { @@ -4165,19 +4167,6 @@ func buildChanPolicy(dbPolicy sqlc.ChannelPolicy, channelID uint64, "fields: %w", err) } - var msgFlags lnwire.ChanUpdateMsgFlags - if dbPolicy.MaxHtlcMsat.Valid { - msgFlags |= lnwire.ChanUpdateRequiredMaxHtlc - } - - var chanFlags lnwire.ChanUpdateChanFlags - if !isNode1 { - chanFlags |= lnwire.ChanUpdateDirection - } - if dbPolicy.Disabled.Bool { - chanFlags |= lnwire.ChanUpdateDisabled - } - var inboundFee fn.Option[lnwire.Fee] if dbPolicy.InboundFeeRateMilliMsat.Valid || dbPolicy.InboundBaseFeeMsat.Valid { @@ -4194,8 +4183,12 @@ func buildChanPolicy(dbPolicy sqlc.ChannelPolicy, channelID uint64, LastUpdate: time.Unix( dbPolicy.LastUpdate.Int64, 0, ), - MessageFlags: msgFlags, - ChannelFlags: chanFlags, + MessageFlags: sqldb.ExtractSqlInt16[lnwire.ChanUpdateMsgFlags]( + dbPolicy.MessageFlags, + ), + ChannelFlags: sqldb.ExtractSqlInt16[lnwire.ChanUpdateChanFlags]( + dbPolicy.ChannelFlags, + ), TimeLockDelta: uint16(dbPolicy.Timelock), MinHTLC: lnwire.MilliSatoshi( dbPolicy.MinHtlcMsat, @@ -4259,6 +4252,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat, Disabled: r.Policy1Disabled, + MessageFlags: r.Policy1MessageFlags, + ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, } } @@ -4277,6 +4272,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat, Disabled: r.Policy2Disabled, + MessageFlags: r.Policy2MessageFlags, + ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, } } @@ -4299,6 +4296,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat, Disabled: r.Policy1Disabled, + MessageFlags: r.Policy1MessageFlags, + ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, } } @@ -4317,6 +4316,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat, Disabled: r.Policy2Disabled, + MessageFlags: r.Policy2MessageFlags, + ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, } } @@ -4339,6 +4340,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat, Disabled: r.Policy1Disabled, + MessageFlags: r.Policy1MessageFlags, + ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, } } @@ -4357,6 +4360,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat, Disabled: r.Policy2Disabled, + MessageFlags: r.Policy2MessageFlags, + ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, } } @@ -4379,6 +4384,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat, Disabled: r.Policy1Disabled, + MessageFlags: r.Policy1MessageFlags, + ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, } } @@ -4397,6 +4404,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat, Disabled: r.Policy2Disabled, + MessageFlags: r.Policy2MessageFlags, + ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, } } @@ -4419,6 +4428,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy1InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy1InboundFeeRateMilliMsat, Disabled: r.Policy1Disabled, + MessageFlags: r.Policy1MessageFlags, + ChannelFlags: r.Policy1ChannelFlags, Signature: r.Policy1Signature, } } @@ -4437,6 +4448,8 @@ func extractChannelPolicies(row any) (*sqlc.ChannelPolicy, *sqlc.ChannelPolicy, InboundBaseFeeMsat: r.Policy2InboundBaseFeeMsat, InboundFeeRateMilliMsat: r.Policy2InboundFeeRateMilliMsat, Disabled: r.Policy2Disabled, + MessageFlags: r.Policy2MessageFlags, + ChannelFlags: r.Policy2ChannelFlags, Signature: r.Policy2Signature, } } diff --git a/sqldb/sqlutils.go b/sqldb/sqlutils.go index bafba8416..8745fe2f7 100644 --- a/sqldb/sqlutils.go +++ b/sqldb/sqlutils.go @@ -68,3 +68,10 @@ func SQLTime(t time.Time) sql.NullTime { Valid: true, } } + +// ExtractSqlInt16 turns a NullInt16 into a numerical type. This can be useful +// when reading directly from the database, as this function handles extracting +// the inner value from the "option"-like struct. +func ExtractSqlInt16[T constraints.Integer](num sql.NullInt16) T { + return T(num.Int16) +} From c5b2e4e50af4810b01e25bb03f38a89a3876414b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 30 Jun 2025 12:33:43 +0200 Subject: [PATCH 6/6] graph/db: update outdated comments --- graph/db/test_postgres.go | 3 +-- graph/db/test_sqlite.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/graph/db/test_postgres.go b/graph/db/test_postgres.go index 11144eb3b..8067756e2 100644 --- a/graph/db/test_postgres.go +++ b/graph/db/test_postgres.go @@ -12,8 +12,7 @@ import ( ) // NewTestDB is a helper function that creates a SQLStore backed by a postgres -// database for testing. At the moment, it embeds a KVStore but once the -// SQLStore fully implements the V1Store interface, the KVStore will be removed. +// database for testing. func NewTestDB(t testing.TB) V1Store { pgFixture := sqldb.NewTestPgFixture( t, sqldb.DefaultPostgresFixtureLifetime, diff --git a/graph/db/test_sqlite.go b/graph/db/test_sqlite.go index f9cb5a46d..76b59c84b 100644 --- a/graph/db/test_sqlite.go +++ b/graph/db/test_sqlite.go @@ -12,8 +12,7 @@ import ( ) // NewTestDB is a helper function that creates a SQLStore backed by a sqlite -// database for testing. At the moment, it embeds a KVStore but once the -// SQLStore fully implements the V1Store interface, the KVStore will be removed. +// database for testing. func NewTestDB(t testing.TB) V1Store { db := sqldb.NewTestSqliteDB(t).BaseDB