Skip to content

Conversation

@martinserts
Copy link
Contributor

@martinserts martinserts commented Dec 18, 2025

Description

Migrates DB layer from sqlx to rusqlite.

It uses serde_rusqlite for auto wrapping/unwrapping for DB types.
rusqlite_migration is used for migrations.

Closes tari-project/tari#7643

Motivation and Context

We are planning to use minotari-cli as a library. sqlx was making it hard.

How Has This Been Tested?

$ cargo run --bin minotari -- import-view-key  \ 
  --view-private-key <hidden> \ 
  --spend-public-key <hidden> \
  --password password \
  --database-file data/wallet.db \
  --birthday 1400
Importing wallet with view key: <hidden> and spend key: <hidden>
$ cargo run --bin minotari -- scan \ 
  --password password \ 
  --account-name default \ 
  --max-blocks-to-scan 500000 \ 
  --base-url https://rpc.tari.com \
  --database-file data/wallet.db \
  --batch-size 100   
Scanning blockchain...
Starting scan for account 1 from height 128879
Processing 100 scanned blocks for account 1                                                                           
Scan complete. Events: 14
$ cargo run --bin minotari   -- balance -a default
Fetching balance...                                                                                                   
Balance at height 156148(2025-12-11 09:27:41): 690 microTari (0.690 Tari)
$ cargo run --bin minotari   -- daemon \ 
  --password password \ 
  --base-url https://rpc.tari.com \ 
  --database-file data/wallet.db \ 
  --batch-size 100 \ 
  --network mainnet
Starting Tari wallet daemon...
Daemon started. Press Ctrl+C to stop.
Transaction unlocker task started.
API server listening on 0.0.0.0:9000
Starting wallet scan...
No reorgs detected.
Starting scan for account 1 from height 161100
Scan completed successfully. Found 0 events.
curl -X 'GET' \
  'http://localhost:9000/accounts/default/balance' \
  -H 'accept: application/json'
{
  "total": 690,
  "available": 690,
  "locked": 0,
  "unconfirmed": 0,
  "total_credits": 1058954880,
  "total_debits": 1058954190,
  "max_height": 156148,
  "max_date": "2025-12-11 09:27:41"
}
curl -X 'POST' \
  'http://localhost:9000/accounts/default/lock_funds' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "amount": 1000,
  "confirmation_window": 3,
  "estimated_output_size": 0,
  "fee_per_gram": 5,
  "idempotency_key": "lock_1000",
  "num_outputs": 1,
  "seconds_to_lock_utxos": 86400
}'
{
  "error": "Not enough funds. Available: 690 µT, required: 1620 µT"
}
curl -X 'POST' \
  'http://localhost:9000/accounts/default/lock_funds' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "amount": 50,
  "confirmation_window": 3,
  "estimated_output_size": 0,
  "fee_per_gram": 5,
  "idempotency_key": "lock_50",
  "num_outputs": 1,
  "seconds_to_lock_utxos": 86400
}'
{
  "utxos": [
    {
      "version": "V0",
      "value": 690,
      "commitment_mask_key_id": {
        "Encrypted": {
          "encrypted": [
            28,
            117,
            129,
            236,
            32,
            79,
            33,
            102,
            89,
            46,
            254,
            141,
            111,
            246,
            112,
            63,
            216,
            164,
            86,
            210,
            229,
            117,
            14,
            142,
            173,
            153,
            120,
            160,
            241,
            137,
            219,
            26,
            48,
            203,
            210,
            122,
            137,
            53,
            36,
            50,
            190,
            46,
            54,
            237,
            42,
            126,
            207,
            186,
            120,
            61,
            110,
            219,
            98,
            123,
            91,
            0,
            138,
            63,
            149,
            68,
            163,
            83,
            175,
            200,
            9,
            230,
            246,
            69,
            121,
            34,
            144,
            243
          ],
          "key": {
            "inner": "view_key"
          }
        }
      },
      "features": {
        "version": "V0",
        "output_type": 0,
        "maturity": 0,
        "coinbase_extra": "",
        "sidechain_feature": null,
        "range_proof_type": "bullet_proof_plus"
      },
      "script": "7e66a39c2f732cdf850f813113fadb168b66efa6b037fa8da46adb6a67b3f3d119",
      "covenant": "",
      "input_data": "",
      "script_key_id": {
        "Derived": {
          "key": {
            "inner": "encrypted.1c7581ec204f2166592efe8d6ff6703fd8a456d2e5750e8ead9978a0f189db1a30cbd27a89352432be2e36ed2a7ecfba783d6edb627b5b008a3f9544a353afc809e6f645792290f3.view_key"
          }
        }
      },
      "sender_offset_public_key": "0c5d53464985c23f5fc44847cf41ad407c2de1e13106e01d71126e54266e1078",
      "metadata_signature": {
        "ephemeral_commitment": "3a237c51d72352f1ec72d5d4e3ea9922b40085c6fd17682aa3a6cc29a3570749",
        "ephemeral_pubkey": "10af1f2bac2e479822ebb170215d56eb3ea595d5e1c1adfc45774202395f121c",
        "u_a": "1eff4fa81f84e406e5ee215510e52f03601e4bcbf49dfc0297382c52d1a3ce07",
        "u_x": "d7005ed1749ebf9aa10d5700d05afa0fc6582926d071109994c121501a233a06",
        "u_y": "0158ad9c61af0173c26fe445e898e4e087b2757530ec50517aa42e69dd8dfd05"
      },
      "script_lock_height": 0,
      "encrypted_data": {
        "data": "3bb0d5f5a8de9b1dc345410c43d3996352c60ef6c06c4eaa71fbf037930b6a95286d04dc5ac5cdebd141e645639504b77452864a66e4d009c32d7d389546e1cc79acef8c41ba2e56612e8b9c7d7235bdea092c9c1ce737e6d94e32a806908f5db9a39f8171ffcf3a4e70cfef4773522823b10151b00bedb3889088516762637ead17535f5453c393cb13c9f3ae351c1ec71e0d7723b608874c425c152bf0f882a5e5fcf65fbb2f932767492676c87824b9f6e06eafa9e29da42ece46a2ca05b0fb688abbcc89d04ad923366a4c4e0c400d55"
      },
      "minimum_value_promise": 0,
      "range_proof": "018153f8a01dd9640bc7f7f16d8985b5f1180048a047d6581503f2200eec7d60073c3106750e169d7f8044b03633bb6f69e2cf308dcfa0c6ad4517147f68bd197c9c505161e628397f1da8d266d301ca756226180a488173984c5077b3a5469865ba2ffd563b91bf3057b352531052bfc504661d490aaff6dc574831f83c71f6435734790d8b01e544c2f3f813edaa7d6db83d043f0600ee9cfa2982d3c29bc50e5a0cf3db44aa2e2993fbdd903ff986ac3f8aa7c9000ea7c118dccc16c980440e6cafdd5c4421bda6fcb2012645828f1a53b0a56c4273b4d3b5b07352d73e6900f4f6cd3d2b7d62fbafd9d720141cea3187c1f27cac43fe25f7d7bfd3b65fb94df2f8478439c9d7a47a4ec1c584823ba101f1cf9b00cb10b2e2cb8b6f5990654206c42207533f111ca79add574a500999a85267afde89cc45946aeea704494f7ce41a2c249bf979bfd08c6a42db374ed1aaee0af7d6c2f7361dad9f4c10b852652e98ca5f59491dda56ab2c2027873ae441d088f06bb1cbf437351ce3ab5e8245eacc041f74dd09592a1c2fee9ca1543bbcc3f35a178c186c21740090ea4555078640629f4650ba7c9fce520f4fdab503a000d822b351ff98593d47ec86af9e2200a012ae4e1d761b1acfb43d7a4763c328383c44eb4d8102b344374cf96c584f068c75b30bb355fd3f5067f80e5163ff91b8b1d693b51f84377dee5c118e572e78b39c86e7969a60f37d19a0aef7a34406781fc82bd65c6a0129b41af421b66fd8691085f2b6b06841b786351799221b72ed4171cab932c7e5501689a8773b12",
      "payment_id": {
        "inner": {
          "TransactionInfo": {
            "recipient_address": "12BS6NPHQWS7Xg66dDj5o7gC1t3dSvwZAqgxXsuEwwgZWUVY5LYDeWCRpbm1bTjdbvpyKJr3S1tDry6NXMfkLDZacJR",
            "sender_one_sided": true,
            "amount": 84964000,
            "fee": 660,
            "tx_type": "PaymentToOther",
            "sent_output_hashes": [
              [
                117,
                249,
                186,
                3,
                48,
                25,
                229,
                250,
                188,
                218,
                97,
                255,
                44,
                177,
                102,
                200,
                119,
                234,
                174,
                92,
                41,
                89,
                46,
                80,
                223,
                13,
                60,
                97,
                61,
                231,
                102,
                31
              ]
            ],
            "payment_id": [
              67,
              104,
              97,
              110,
              103,
              101
            ]
          }
        }
      },
      "output_hash": [
        30,
        185,
        128,
        62,
        225,
        227,
        196,
        221,
        32,
        253,
        89,
        17,
        77,
        201,
        153,
        172,
        152,
        101,
        177,
        40,
        47,
        95,
        16,
        230,
        42,
        181,
        251,
        55,
        242,
        119,
        90,
        202
      ],
      "commitment": "506e50bf93de6ee2570fd6f181c1605b2128cc9044a3e5bbe50a29d020cb8a32"
    }
  ],
  "requires_change_output": true,
  "total_value": 690,
  "fee_without_change": 355,
  "fee_with_change": 620
}

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

Breaking Changes

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @martinserts, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request undertakes a significant refactoring of the application's database layer, transitioning from sqlx to rusqlite. This change is driven by the need to enhance the reusability of minotari-cli as a library, which was previously hindered by sqlx. The migration involved a comprehensive update of dependencies, a rewrite of all database interaction code across the API, daemon, and core database modules to use rusqlite's synchronous API, and the adoption of rusqlite_migration for schema management. The new setup also integrates serde_rusqlite for efficient data handling and r2d2 for robust connection pooling.

Highlights

  • Database Layer Migration: The core database layer has been migrated from sqlx to rusqlite to improve library compatibility, particularly for minotari-cli.
  • New Database Dependencies: The project now utilizes rusqlite for SQLite interactions, serde_rusqlite for automatic data serialization/deserialization, rusqlite_migration for managing database schema changes, and r2d2 with r2d2_sqlite for connection pooling.
  • Synchronous Database Operations: All database interaction functions have been converted from async to sync to align with rusqlite's synchronous nature, with connection pooling handled by r2d2.
  • Database Error Handling: A new WalletDbError enum and WalletDbResult type have been introduced to provide a unified and more granular error handling mechanism for database operations.
  • Migration System Update: Database migration scripts have been reorganized and renamed to a numerical format (e.g., 00001-init/up.sql) to be compatible with rusqlite_migration, and a mechanism for adopting existing sqlx migrations has been added.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@martinserts martinserts self-assigned this Dec 18, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant refactoring that migrates the database layer from the asynchronous sqlx library to the synchronous rusqlite library, using r2d2 for connection pooling. This is a good move to simplify the database logic. However, the migration has introduced a critical issue: several parts of the asynchronous codebase now make blocking database calls directly on the async runtime's worker threads. This can lead to severe performance degradation and potential deadlocks. My review focuses on identifying these blocking calls and suggesting the use of tokio::task::spawn_blocking to correctly handle them. I've also noted a potentially destructive behavior in the new database initialization logic.

@martinserts martinserts marked this pull request as ready for review December 19, 2025 09:33
Copy link
Contributor

@stringhandler stringhandler left a comment

Choose a reason for hiding this comment

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

untested review. Mostly looks fine, except for the one that retries a DbError over and over.

I have noted possible swallowing of errors on .next().transpose() but I am not entirely sure if this would result in a swallowing of error, it just looks like it might. If you have tested this, feel free to ignore

println!("An intermittent error occurred during the scan cycle: {}", err_msg);
sleep(self.scan_interval).await;
},
ScanError::DbError(err_msg) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this not be an error that is returned, not retried?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should this not be an error that is returned, not retried?

@stringhandler You are right - fixed 010f69e

)?;

let rows = stmt.query(named_params! { ":name": friendly_name })?;
let row = from_rows::<AccountRow>(rows).next().transpose()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this might swallow an error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this might swallow an error

@stringhandler I think this is alright, and does not swallow the error.

from_rows::<AccountRow>(rows).next() - Option<Result<AccountRow, Error>>
from_rows::<AccountRow>(rows).next().transpose() - Result<Option<AccountRow>, Error>

&self,
password: &str,
) -> Result<(RistrettoSecretKey, CompressedKey<RistrettoPublicKey>), anyhow::Error> {
) -> WalletDbResult<(RistrettoSecretKey, CompressedKey<RistrettoPublicKey>)> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this method is better suited to return an anyhow::Error because of the many errors that can be returned here. But this is a subjective opinion, it can remain as is

Copy link
Contributor Author

@martinserts martinserts Jan 5, 2026

Choose a reason for hiding this comment

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

I think this method is better suited to return an anyhow::Error because of the many errors that can be returned here. But this is a subjective opinion, it can remain as is

@stringhandler
I added another error enum DecryptionFailed(String), and simplified decrypt_keys() so that it may error out only with this error - 312d4b3

If we move to anyhow error here, it will be a chain reaction, and almost all methods will have to be turned from WalletDbResult into anyhow, and we will lose way to distinguish errors.

claimed_sender_address as "claimed_sender_address: _",
balance_credit,
balance_debit,
REPLACE(effective_date, ' ', 'T') as effective_date,
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like it should have been migrated properly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems like it should have been migrated properly

@stringhandler No, it did not work without this replace.

The thing is, that sqlite does not have a date/datetime field. It just stores plain strings, and dates were/are stored as YYYY-MM-DD HH24:MI:SS (with a space).
sqlx was doing some magic in its macros - effective_date as "effective_date: NaiveDateTime"

Now we are using serde_rusqlite, which will deserialize NativeDateTime. And it requires it to be in ISO format.

"PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;",
Copy link
Contributor

Choose a reason for hiding this comment

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

These options need comments justifying their choice

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These options need comments justifying their choice

Great tip - added comments - 4ad1a9a

"Database migration failed: {}. Please, remove database {:?} manually",
e, &path
);
Err(WalletDbError::Unexpected(
Copy link
Contributor

Choose a reason for hiding this comment

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

This error message is misleading. It should rather be "Unexpected error occured. It is possibly a migration from sqlx that has failed. Consider removing database file and try again"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This error message is misleading. It should rather be "Unexpected error occured. It is possibly a migration from sqlx that has failed. Consider removing database file and try again"

@stringhandler You are right, updated the message - d9a026f

.fetch_optional(&mut *conn)
.await?;
let rows = stmt.query(named_params! { ":output_hash": output_hash })?;
let result: Option<OutputInfoRow> = from_rows(rows).next().transpose()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

possible swallowing of error. Looks like you used optional in other places

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same as above.

from_rows(rows).next() - Option<Result<OutputInfoRow, Error>>
from_rows(rows).next().transpose() - Result<Option<OutputInfoRow>, Error>

)?;

let rows = stmt.query(named_params! { ":account_id": account_id })?;
let row = from_rows::<ScannedTipBlockRow>(rows).next().transpose()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

possible swallowing of error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same as above.

from_rows::<ScannedTipBlockRow>(rows).next() - Option<Result<ScannedTipBlockRow, Error>>
from_rows::<ScannedTipBlockRow>(rows).next().transpose() - Result<Option<ScannedTipBlockRow>, Error>

@martinserts martinserts merged commit aa11813 into tari-project:main Jan 5, 2026
2 checks passed
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.

Remove compile time query checkers

3 participants