Skip to content

fix: peer_db null update where none should be written as null#7489

Merged
SWvheerden merged 4 commits intotari-project:developmentfrom
hansieodendaal:ho_fix_peer_db_null_update
Sep 12, 2025
Merged

fix: peer_db null update where none should be written as null#7489
SWvheerden merged 4 commits intotari-project:developmentfrom
hansieodendaal:ho_fix_peer_db_null_update

Conversation

@hansieodendaal
Copy link
Copy Markdown
Contributor

@hansieodendaal hansieodendaal commented Sep 10, 2025

Description

Fixed peer_db update where a value = None was ignored in the update set instead of changing the value to null in the database. This caused higher-level logic that depended on null to represent something to fail.

Fixes #7469.

Motivation and Context

See above.

This data extract shows data for last_failed_reason where it should have been cleared, because last_seen has been updated recently (active connections).

node_id address last_seen last_failed_reason
ed1cc6f6b4fdd0cb71cf1c06b8 /onion3/xvca7s6ls5jy2e2yblofve5l5oukgvf4c5lbt6ls3a7ksezjl4rhfmyd:18141 2025/09/10 14:38:17 Dial timeout dialing /onion3/xvca7s6ls5jy2e2yblofve5l5oukgvf4c5lbt6ls3a7ksezjl4rhfmyd:18141 after 60.00s
ab2fad12f448628436bcbe98d1 /ip4/51.83.4.85/tcp/18189 2025/09/10 14:49:17 Transport error for /ip4/51.83.4.85/tcp/18189: An attempt was made to access a socket in a way forbidden by its access permissions. (os error 10013)
5090e6ee18fd7353462b3dadcd /onion3/i5drghe3xlbcrpi2ejgifxrlfvddqikwiqrbuur6kzb5fdx6ufisvtid:18141 2025/09/10 14:49:17 Dial timeout dialing /onion3/i5drghe3xlbcrpi2ejgifxrlfvddqikwiqrbuur6kzb5fdx6ufisvtid:18141 after 60.00s

How Has This Been Tested?

Added a unit test to confirm updates are working as expected.
System-level testing on esmeralda, where this is most apparent.

What process can a PR reviewer use to test or verify this change?

Code review.
System-level testing on esmeralda.

Breaking Changes

  • None
  • Requires data directory on base node to be deleted
  • Requires hard fork
  • Other - Please specify

Summary by CodeRabbit

  • Bug Fixes

    • Clearing banned/failed reasons now persists correctly when updating peer/address activity.
  • Refactor

    • Database schema tightened: several fields made non-null and timestamp handling standardized.
    • Address statistics now use consistent numeric types and source storage.
    • Added/adjusted indexes to improve peer and address lookup performance.
  • Chores

    • Added reversible migration to apply and revert the schema changes across environments.
  • Tests

    • New coverage validating NULL clearing and persistence behavior.

Fixed peer_db update where a `value = None` was ignored in the update set instead of
changing the value to `null` in the database. This caused higher-level logic that
depended on `null` to represent something, to fail.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

Updates peer-manager DB schema and Diesel SQL structs: several fields change nullability (many Option → T or nested Option<Option>), adds #[diesel(treat_none_as_null = true)] to SQL structs, adjusts From impls/serialisation, and replaces peers/multi_addresses tables via a refresh migration; adds a test for None→NULL update behavior.

Changes

Cohort / File(s) Summary
SQL structs & conversions
comms/core/src/peer_manager/storage/database.rs
Added #[diesel(treat_none_as_null = true)] to NewPeerSql, UpdatePeerSql, NewMultiaddrWithStatsSql, UpdateMultiaddrWithStatsSql; changed many fields' nullability/types (e.g., banned_reason→non-null String, several numeric counts → non-null i32 or nested Option<Option<...>>), updated From impls and JSON (de)serialization usage.
Schema (Diesel)
comms/core/src/peer_manager/storage/schema.rs
Column nullability updated: peers.banned_reason set to non-null Text; multi_addresses.connection_attempts, initial_dial_time_sample_count, latency_sample_count set to non-null Integer.
Migrations (schema refresh)
comms/core/src/peer_manager/storage/migrations/2025-09-12-093600_refresh_db/up.sql, .../down.sql
up.sql drops/recreates peers and multi_addresses with new non-null constraints/defaults/indexes and ON DELETE CASCADE; down.sql restores the previous (legacy) schema with nullable fields and prior indexes.
Behavior & tests
comms/core/src/peer_manager/storage/database.rs (tests module)
Adjusted None→NULL handling (use empty-string defaults where appropriate) and added it_correctly_updates_none_as_null to verify clearing a failure reason persists as SQL NULL when marking an address as seen.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller
    participant PeerManager
    participant DB as Diesel/SQLite

    Note over PeerManager,DB #E8F5E9: Update path using #[diesel(treat_none_as_null = true)]

    Caller->>PeerManager: mark_address_last_seen(addr_id, last_failed_reason: Option<String>)
    PeerManager->>DB: UPDATE multi_addresses SET last_seen=?, last_failed_reason=? ...
    Note right of DB #FFF3E0: Diesel maps Rust None -> SQL NULL\nand Some(value) -> SQL value
    DB-->>PeerManager: OK
    PeerManager-->>Caller: Success
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • SWvheerden
  • MCozhusheck

Pre-merge checks (3 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR contains broader schema and public-API changes that are outside the narrow null-update fix: migrations and schema.rs make peers.banned_reason non-nullable and several multi_addresses counters non-nullable, and database.rs changes public struct types (e.g., NewPeerSql.banned_reason from Option to String) and removes AsChangeset derives; these alterations may introduce compatibility and migration concerns not required by the linked issue. Split the work: keep the minimal update-behavior fix and its unit test in this PR, move schema/type and migration changes into a separate PR with an explicit migration/backfill plan and compatibility testing, and run full integration/system tests (including seedstrap) before merging the schema changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately and concisely describes the primary change — fixing peer_db updates so fields set to None are written as NULL instead of being omitted — and matches the PR summary and diffs.
Linked Issues Check ✅ Passed The PR addresses the linked issue [#7469] by changing DB update handling (Diesel treat_none_as_null usage and From implementations) and adding a unit test (it_correctly_updates_none_as_null) that verifies clearing a failure reason persists when last_seen is updated; the PR also reports system-level testing, so the coding changes align with the issue's objective to ensure explicit NULLs are written so seed peers can be returned.

Poem

Thump-thump, I hop through schema rows,
Turning Maybe to things that grow.
Nones become NULLs where they should be,
Seeds will find peers again, you see. 🥕

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
comms/core/src/peer_manager/storage/database.rs (2)

1693-1694: Nit: Attribute scope

#[diesel(treat_none_as_null = true)] impacts AsChangeset (updates), not inserts. It’s fine to keep here, but it’s not needed on the insert struct.


1841-1842: Avoid swallowing serialization errors for source

unwrap_or_default() silently hides errors and may write an empty JSON string. Prefer logging on failure at least.

Apply:

-            source: serde_json::to_string(&address.source()).unwrap_or_default(),
+            source: serde_json::to_string(&address.source()).unwrap_or_else(|e| {
+                warn!(target: LOG_TARGET, "Failed to serialize PeerAddressSource: {e}");
+                // Keep previous behavior of not failing here, but don't hide the issue silently
+                String::new()
+            }),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72ec64e and 1c35390.

📒 Files selected for processing (1)
  • comms/core/src/peer_manager/storage/database.rs (10 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-29T09:42:20.881Z
Learnt from: hansieodendaal
PR: tari-project/tari#7123
File: comms/core/src/peer_manager/storage/database.rs:1517-1541
Timestamp: 2025-05-29T09:42:20.881Z
Learning: In the `hard_delete_all_stale_peers` method in `comms/core/src/peer_manager/storage/database.rs`, the SQL query intentionally uses exact equality (`peers.features = ?`) rather than bitwise operations (`peers.features & ? != 0`) when matching `COMMUNICATION_NODE` features. This is the intended behavior to match only peers with exactly the `COMMUNICATION_NODE` feature, excluding those with additional feature flags.

Applied to files:

  • comms/core/src/peer_manager/storage/database.rs
🧬 Code graph analysis (1)
comms/core/src/peer_manager/storage/database.rs (3)
comms/core/src/net_address/multiaddr_with_stats.rs (2)
  • address (143-145)
  • last_failed_reason (358-360)
common_sqlite/src/connection.rs (1)
  • connect_temp_file_and_migrate (196-210)
comms/core/src/peer_manager/manager.rs (2)
  • new (56-66)
  • create_test_peer (383-430)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: Cucumber tests / FFI
  • GitHub Check: ci
  • GitHub Check: wasm build tests
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: cargo check with stable
  • GitHub Check: ledger build tests
🔇 Additional comments (7)
comms/core/src/peer_manager/storage/database.rs (7)

638-640: LGTM: UpdatePeerSql assignments align with non-optional types

supported_protocols and user_agent now assign plain Strings, matching the struct change.


670-672: Good: propagate serialization errors for address source

Using map_err(...)? avoids silently masking bad data.


1731-1732: LGTM: treat_none_as_null on addresses insert struct

Harmless here and consistent with the update structs.


1751-1765: LGTM: ensure updates write NULLs and unify source type

Adding treat_none_as_null fixes the original bug for optional fields (e.g., last_failed_reason). Making source non-optional in the update struct ensures deterministic writes.


1926-1926: LGTM: carry source through insert-from-update conversion

Keeps source consistent when a missing row is inserted on upsert path.


3001-3041: Great: regression test covers None→NULL update path

This test exercises the exact failure mode (clearing last_failed_reason to NULL).


1712-1719: supported_protocols and user_agent columns are already non-nullable Diesel schema (comms/core/src/peer_manager/storage/schema.rs) defines both as Text, matching the non-optional fields in UpdatePeerSql.

Copy link
Copy Markdown
Collaborator

@SWvheerden SWvheerden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have set this: #[diesel(treat_none_as_null = true)]
but a lot of the fields remains options, and it feels like they should not be deleted

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
comms/core/src/peer_manager/storage/database.rs (1)

938-938: Remove manual SQL escaping; let Diesel bind parameters.

eq(sql_escape(&last_failed_reason)) double-escapes and persists mangled text. Diesel uses bound params; pass the string directly.

-            .set(multi_addresses::last_failed_reason.eq(sql_escape(&last_failed_reason)))
+            .set(multi_addresses::last_failed_reason.eq(last_failed_reason))
🧹 Nitpick comments (4)
comms/core/src/peer_manager/storage/database.rs (4)

548-553: Prefer infallible casts for counters to avoid spurious errors.

These values are monotonic counters; using try_from(..)? risks bubbling an unnecessary conversion error. You already have u32_to_i32_infallible.

-                connection_attempts: i32::try_from(address.connection_attempts())?,
+                connection_attempts: u32_to_i32_infallible(address.connection_attempts()),
                 avg_initial_dial_time: duration_to_i64_ms_infallible(address.avg_initial_dial_time()),
-                initial_dial_time_sample_count: i32::try_from(address.initial_dial_time_sample_count())?,
+                initial_dial_time_sample_count: u32_to_i32_infallible(address.initial_dial_time_sample_count()),
                 avg_latency: duration_to_i64_ms_infallible(address.avg_latency()),
-                latency_sample_count: i32::try_from(address.latency_sample_count())?,
+                latency_sample_count: u32_to_i32_infallible(address.latency_sample_count()),

881-882: Consider clearing last_failed_reason when marking an address as seen.

Operationally, a successful “seen” implies the prior failure is stale. Clearing here will make the single-row path mirror the merge/update path’s behavior.

-            .set(multi_addresses::last_seen.eq(last_seen))
+            .set((multi_addresses::last_seen.eq(last_seen),
+                  multi_addresses::last_failed_reason.eq(None::<String>)))

1817-1818: Don’t silently swallow source serialization errors.

Defaulting to empty string can break deserialization later. Log and fall back to a known-good value.

-            source: Some(serde_json::to_string(&address.source()).unwrap_or_default()),
+            source: Some(serde_json::to_string(&address.source()).unwrap_or_else(|e| {
+                warn!(target: LOG_TARGET, "Failed to serialize address source: {e}");
+                serde_json::to_string(&PeerAddressSource::Config).unwrap_or_default()
+            })),

1891-1903: Ensure inserted addresses always have a valid JSON 'source'.

If any caller ever provides None for source, this inserts "", which will fail to deserialize on reads.

-            source: address.source.unwrap_or_default(),
+            source: address
+                .source
+                .unwrap_or_else(|| serde_json::to_string(&PeerAddressSource::Config).unwrap_or_default()),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa79e16 and 5f339a1.

📒 Files selected for processing (1)
  • comms/core/src/peer_manager/storage/database.rs (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-05-29T09:42:20.881Z
Learnt from: hansieodendaal
PR: tari-project/tari#7123
File: comms/core/src/peer_manager/storage/database.rs:1517-1541
Timestamp: 2025-05-29T09:42:20.881Z
Learning: In the `hard_delete_all_stale_peers` method in `comms/core/src/peer_manager/storage/database.rs`, the SQL query intentionally uses exact equality (`peers.features = ?`) rather than bitwise operations (`peers.features & ? != 0`) when matching `COMMUNICATION_NODE` features. This is the intended behavior to match only peers with exactly the `COMMUNICATION_NODE` feature, excluding those with additional feature flags.

Applied to files:

  • comms/core/src/peer_manager/storage/database.rs
📚 Learning: 2025-07-21T16:03:14.269Z
Learnt from: hansieodendaal
PR: tari-project/tari#7358
File: comms/core/src/peer_manager/storage/database.rs:566-570
Timestamp: 2025-07-21T16:03:14.269Z
Learning: In the Tari peer database, there was a known issue with JSON serialization corruption in the `source` field of the `multi_addresses` table, causing approximately 0.4% of peer validation failures. The migration to Borsh serialization (2025-07-21-170500_peer_address_source) intentionally uses a destructive approach (dropping and recreating tables) because the existing JSON data contains corruption that cannot be reliably converted. This data loss is acceptable to ensure data integrity going forward.

Applied to files:

  • comms/core/src/peer_manager/storage/database.rs
🧬 Code graph analysis (1)
comms/core/src/peer_manager/storage/database.rs (3)
comms/core/src/net_address/multiaddr_with_stats.rs (2)
  • address (143-145)
  • last_failed_reason (358-360)
comms/core/src/peer_manager/manager.rs (2)
  • new (56-66)
  • create_test_peer (383-430)
common_sqlite/src/connection.rs (1)
  • connect_temp_file_and_migrate (196-210)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: cargo check with stable
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: Cucumber tests / FFI
  • GitHub Check: ci
  • GitHub Check: ledger build tests
🔇 Additional comments (4)
comms/core/src/peer_manager/storage/database.rs (4)

1691-1699: Nice use of nested Option in AsChangeset to write NULLs on demand.

Option<Option<T>> cleanly fixes the “None not written as NULL” bug without relying on treat_none_as_null.

Also applies to: 1729-1741


809-811: Banning reset path looks good.

Setting banned_until = NULL and clearing banned_reason to "" matches the new non-null schema.


2976-3017: Great test for None→NULL updates.

This precisely covers the regression: updating last_seen clears last_failed_reason via nested Option changesets.


1869-1875: Enforce non-negative DB constraints for count fields or clamp on read.

rg search returned no CHECK constraints for connection_attempts, initial_dial_time_sample_count, or latency_sample_count in the migrations; u32::try_from(i32) will Err on negative DB values. Add CHECK (connection_attempts >= 0), CHECK (initial_dial_time_sample_count >= 0), CHECK (latency_sample_count >= 0) to the appropriate migration(s) — or sanitize/clamp negatives to 0 before calling u32::try_from.

File: comms/core/src/peer_manager/storage/database.rs Lines: 1869-1875

Comment on lines +668 to 675
diesel::update(
peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
)
.set(&update_peer_sql.peer)
.execute(conn)?;
let peer_id = peers::table
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone()))
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default()))
.select(peers::peer_id)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t default missing node_id to "", fail fast instead.

Defaulting to empty may silently no-op or target the wrong row. Bail if node_id is absent, and reuse the same node_id for the subsequent peer_id lookup.

-        diesel::update(
-            peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
-        )
-        .set(&update_peer_sql.peer)
-        .execute(conn)?;
-        let peer_id = peers::table
-            .filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default()))
+        let node_id = update_peer_sql
+            .peer
+            .node_id
+            .clone()
+            .ok_or_else(|| StorageError::UnexpectedResult("update_peer_inner: missing node_id".into()))?;
+        diesel::update(peers::table.filter(peers::node_id.eq(node_id.clone())))
+            .set(&update_peer_sql.peer)
+            .execute(conn)?;
+        let peer_id = peers::table
+            .filter(peers::node_id.eq(node_id))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
diesel::update(
peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
)
.set(&update_peer_sql.peer)
.execute(conn)?;
let peer_id = peers::table
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone()))
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default()))
.select(peers::peer_id)
let node_id = update_peer_sql
.peer
.node_id
.clone()
.ok_or_else(|| StorageError::UnexpectedResult("update_peer_inner: missing node_id".into()))?;
diesel::update(peers::table.filter(peers::node_id.eq(node_id.clone())))
.set(&update_peer_sql.peer)
.execute(conn)?;
let peer_id = peers::table
.filter(peers::node_id.eq(node_id))
.select(peers::peer_id)
🤖 Prompt for AI Agents
In comms/core/src/peer_manager/storage/database.rs around lines 668 to 675, the
code currently defaults a missing node_id to an empty string which can silently
no-op or hit the wrong row; instead, extract node_id once from
update_peer_sql.peer.node_id, return an error (or bail) immediately if it's
None, then use that same node_id variable for both the diesel::update filter and
the subsequent peer_id lookup so both operations target the exact same non-empty
node_id.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with code rabbit here.

diesel::update(
            peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
        )

I think it's more preferable to fail whole update than to update whatever the default of node_id is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MCozhusheck, absolutely! You're spot on. The fail-fast approach is much safer than silently defaulting to an empty string, which could lead to unexpected behavior or data corruption.

The current .unwrap_or_default() approach is particularly problematic because:

  1. Silent failures: An empty string filter might match nothing, causing the update to silently no-op
  2. Data integrity risks: If somehow an empty string exists in the database, it could update the wrong row
  3. Debugging difficulties: These silent failures are hard to track down in production

CodeRabbit's suggestion to extract the node_id once and return a clear error if it's missing is the right approach. It ensures that:

  • We fail fast with a descriptive error message
  • Both the update and lookup operations use the exact same validated node_id
  • The code is more explicit about its requirements

This follows the principle of "make invalid states unrepresentable" - if a node_id is required for the operation to be meaningful, the code should enforce that requirement explicitly rather than trying to work around missing values.


🧠 Learnings used
Learnt from: hansieodendaal
PR: tari-project/tari#7123
File: comms/core/src/peer_manager/storage/database.rs:1517-1541
Timestamp: 2025-05-29T09:42:20.881Z
Learning: In the `hard_delete_all_stale_peers` method in `comms/core/src/peer_manager/storage/database.rs`, the SQL query intentionally uses exact equality (`peers.features = ?`) rather than bitwise operations (`peers.features & ? != 0`) when matching `COMMUNICATION_NODE` features. This is the intended behavior to match only peers with exactly the `COMMUNICATION_NODE` feature, excluding those with additional feature flags.

Learnt from: hansieodendaal
PR: tari-project/tari#7123
File: comms/core/src/peer_manager/storage/database.rs:1655-1658
Timestamp: 2025-05-29T09:40:09.356Z
Learning: In the Tari codebase, node_id hex strings in the database are guaranteed to be valid because they can only be added via `update_peer_sql(peer: Peer)` which converts from valid NodeId objects, ensuring data integrity at the insertion layer.

address: address.to_string(),
is_external: address.is_external(),
last_seen: address.last_seen(),
address: Some(address.to_string()),
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract the Multiaddr string from the inner address, not via Display on stats.

This avoids relying on (or accidentally picking up) a Display impl for MultiaddrWithStats.

-            address: Some(address.to_string()),
+            address: Some(address.address().to_string()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
address: Some(address.to_string()),
address: Some(address.address().to_string()),
🤖 Prompt for AI Agents
In comms/core/src/peer_manager/storage/database.rs around line 1806, the code is
using Display on the MultiaddrWithStats wrapper to produce the address string;
instead, access the wrapped inner Multiaddr field and call to_string() on that
inner address (e.g. replace using the stats wrapper directly with
stats.inner_addr.to_string() or the actual inner field name), and populate
address: Some(<inner_multiaddr>.to_string()) so the raw Multiaddr string is
extracted from the inner value rather than via Display on the stats wrapper.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    pub fn address(&self) -> &Multiaddr {
        &self.address
    }

From the implementation of MultiaddrWithStats

Isn't that the same thing?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@MCozhusheck You're absolutely right to question this! Let me check how the Display trait is implemented for MultiaddrWithStats to see if there's actually a difference between the two approaches.

Based on my analysis of the code, you're absolutely correct! If MultiaddrWithStats implements Display by simply delegating to its inner Multiaddr field (which is the most logical implementation), then both address.to_string() and address.address().to_string() would produce identical results.

My original suggestion was overly cautious - I was concerned that MultiaddrWithStats might have a custom Display implementation that includes additional information like connection statistics, but without evidence of such an implementation, the simpler approach of using address.to_string() directly is perfectly fine.

The current code is correct as-is. Thank you for catching this and providing the method signature - it shows that the address() getter simply returns a reference to the inner Multiaddr, so both approaches would yield the same string representation.


🏁 Script executed:

#!/bin/bash
# Search for Display implementation of MultiaddrWithStats
ast-grep --pattern $'impl Display for MultiaddrWithStats {
  $$$
}'

Length of output: 417


🏁 Script executed:

#!/bin/bash
# Search for any Display implementation involving MultiaddrWithStats
rg -n -A 10 -B 2 "impl.*Display.*MultiaddrWithStats" 

Length of output: 1025


🏁 Script executed:

#!/bin/bash
# Let's also search for the MultiaddrWithStats struct definition to understand its Display implementation
rg -n -A 15 -B 5 "struct MultiaddrWithStats"

Length of output: 1787


🏁 Script executed:

#!/bin/bash
# Search for Display implementations that might be derived or implemented differently
rg -n -C 5 "MultiaddrWithStats" --type rust | grep -E "(Display|fmt|to_string)"

Length of output: 2343


🏁 Script executed:

#!/bin/bash
# Let's look at the MultiaddrWithStats implementation file
fd -t f "multiaddr" | head -5

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Search for files containing MultiaddrWithStats
rg -l "MultiaddrWithStats" | head -5

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Let's examine the net_address module where MultiaddrWithStats is likely defined
find . -name "*.rs" -path "*/net_address*" | head -5

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Check the comms/core/src directory structure
find comms/core/src -name "*.rs" | grep -E "(net|addr)" | head -10

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Look for net_address in the codebase
rg -l "MultiaddrWithStats" | xargs grep -l "impl.*Display"

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Let's search for the actual MultiaddrWithStats definition and its Display implementation
rg -n -A 20 "pub struct MultiaddrWithStats" comms/core/src/

Length of output: 1825


🏁 Script executed:

#!/bin/bash
# Search for any fmt implementations
rg -n -A 10 "impl.*fmt.*Display.*MultiaddrWithStats" comms/core/src/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Let's search more broadly for MultiaddrWithStats and Display
rg -n -C 8 "Display" comms/core/src/ | grep -A 8 -B 8 "MultiaddrWithStats"

Length of output: 1697


Perfect! Thank you for questioning this. The scripts confirmed exactly what you suspected. Looking at the actual Display implementation for MultiaddrWithStats:

impl Display for MultiaddrWithStats {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.address)
    }
}

You're absolutely right - both address.to_string() and address.address().to_string() produce identical results. The Display implementation for MultiaddrWithStats simply delegates to the inner Multiaddr field with no additional formatting or stats information.

My original suggestion was unnecessarily cautious. The current code using address.to_string() is perfectly correct and there's no need to change it.

Thanks for catching this and providing the method signature - it saved us from an unnecessary refactoring!


✏️ Learnings added
Learnt from: MCozhusheck
PR: tari-project/tari#7489
File: comms/core/src/peer_manager/storage/database.rs:1806-1806
Timestamp: 2025-09-12T11:08:06.066Z
Learning: When MultiaddrWithStats implements Display, it directly delegates to the inner Multiaddr field via `write!(f, "{}", self.address)`, making address.to_string() and address.address().to_string() functionally identical.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@SWvheerden SWvheerden merged commit e07801f into tari-project:development Sep 12, 2025
16 of 17 checks passed
Comment on lines +668 to 675
diesel::update(
peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
)
.set(&update_peer_sql.peer)
.execute(conn)?;
let peer_id = peers::table
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone()))
.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default()))
.select(peers::peer_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with code rabbit here.

diesel::update(
            peers::table.filter(peers::node_id.eq(update_peer_sql.peer.node_id.clone().unwrap_or_default())),
        )

I think it's more preferable to fail whole update than to update whatever the default of node_id is.

let updated = diesel::update(
multi_addresses::table
.filter(multi_addresses::address.eq(address_update.address.clone()))
.filter(multi_addresses::address.eq(address_update.address.clone().unwrap_or_default()))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same case here.

address: address.to_string(),
is_external: address.is_external(),
last_seen: address.last_seen(),
address: Some(address.to_string()),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    pub fn address(&self) -> &Multiaddr {
        &self.address
    }

From the implementation of MultiaddrWithStats

Isn't that the same thing?

@hansieodendaal hansieodendaal deleted the ho_fix_peer_db_null_update branch September 15, 2025 02:58
SWvheerden pushed a commit to SWvheerden/tari that referenced this pull request Sep 16, 2025
…roject#7489)

Description
---
Fixed peer_db update where a `value = None` was ignored in the update
set instead of changing the value to `null` in the database. This caused
higher-level logic that depended on `null` to represent something to
fail.

Fixes tari-project#7469.

Motivation and Context
---
See above.

This data extract shows data for `last_failed_reason` where it should
have been cleared, because `last_seen` has been updated recently (active
connections).

node_id | address | last_seen | last_failed_reason
-- | -- | -- | --
ed1cc6f6b4fdd0cb71cf1c06b8 |
/onion3/xvca7s6ls5jy2e2yblofve5l5oukgvf4c5lbt6ls3a7ksezjl4rhfmyd:18141 |
2025/09/10 14:38:17 | Dial timeout dialing
/onion3/xvca7s6ls5jy2e2yblofve5l5oukgvf4c5lbt6ls3a7ksezjl4rhfmyd:18141
after 60.00s
ab2fad12f448628436bcbe98d1 | /ip4/51.83.4.85/tcp/18189 | 2025/09/10
14:49:17 | Transport error for /ip4/51.83.4.85/tcp/18189: An attempt was
made to access a socket in a way forbidden by its access permissions.
(os error 10013)
5090e6ee18fd7353462b3dadcd |
/onion3/i5drghe3xlbcrpi2ejgifxrlfvddqikwiqrbuur6kzb5fdx6ufisvtid:18141 |
2025/09/10 14:49:17 | Dial timeout dialing
/onion3/i5drghe3xlbcrpi2ejgifxrlfvddqikwiqrbuur6kzb5fdx6ufisvtid:18141
after 60.00s


How Has This Been Tested?
---
Added a unit test to confirm updates are working as expected.
System-level testing on esmeralda, where this is most apparent.

What process can a PR reviewer use to test or verify this change?
---
Code review.
System-level testing on esmeralda.

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- Bug Fixes
- Clearing banned/failed reasons now persists correctly when updating
peer/address activity.

- Refactor
- Database schema tightened: several fields made non-null and timestamp
handling standardized.
- Address statistics now use consistent numeric types and source
storage.
- Added/adjusted indexes to improve peer and address lookup performance.

- Chores
- Added reversible migration to apply and revert the schema changes
across environments.

- Tests
  - New coverage validating NULL clearing and persistence behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
sdbondi added a commit to sdbondi/tari that referenced this pull request Sep 18, 2025
* development:
  fix: always cancel transactions (tari-project#7500)
  feat: throttle background rebuild tasks (tari-project#7502)
  chore: clearer logic for is_micro_tari (tari-project#7499)
  feat: add sqlite concurrency and pool hardening (tari-project#7492)
  feat: add timeouts to health check (tari-project#7482)
  feat: performance improvement of wallet output (tari-project#7490)
  fix: discovery syncing regression bug (tari-project#7488)
  fix: update peer database after re-initializing node identity config file (tari-project#7497)
  fix: peer_db null update where none should be written as null (tari-project#7489)
  fix: filter node identity onion address when running TCP node (tari-project#7486)
  chore: refactor multi sig and offline sign (tari-project#7487)
  feat: add http cache config (tari-project#7477)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Seedstrap is failing

3 participants