@@ -19,16 +19,17 @@ use std::time::SystemTime;
19
19
20
20
use blockstack_lib:: chainstate:: nakamoto:: { NakamotoBlock , NakamotoBlockVote } ;
21
21
use blockstack_lib:: util_lib:: db:: {
22
- query_row, sqlite_open, table_exists, u64_to_sql, Error as DBError ,
22
+ query_row, sqlite_open, table_exists, tx_begin_immediate , u64_to_sql, Error as DBError ,
23
23
} ;
24
24
use clarity:: types:: chainstate:: BurnchainHeaderHash ;
25
25
use clarity:: util:: get_epoch_time_secs;
26
26
use libsigner:: BlockProposal ;
27
- use rusqlite:: { params, Connection , Error as SqliteError , OpenFlags } ;
27
+ use rusqlite:: {
28
+ params, Connection , Error as SqliteError , OpenFlags , OptionalExtension , Transaction ,
29
+ } ;
28
30
use serde:: { Deserialize , Serialize } ;
29
31
use slog:: { slog_debug, slog_error} ;
30
32
use stacks_common:: types:: chainstate:: ConsensusHash ;
31
- use stacks_common:: types:: sqlite:: NO_PARAMS ;
32
33
use stacks_common:: util:: hash:: Sha512Trunc256Sum ;
33
34
use stacks_common:: { debug, error} ;
34
35
use wsts:: net:: NonceRequest ;
@@ -107,7 +108,7 @@ pub struct SignerDb {
107
108
db : Connection ,
108
109
}
109
110
110
- const CREATE_BLOCKS_TABLE : & str = "
111
+ static CREATE_BLOCKS_TABLE : & str = "
111
112
CREATE TABLE IF NOT EXISTS blocks (
112
113
reward_cycle INTEGER NOT NULL,
113
114
signer_signature_hash TEXT NOT NULL,
@@ -119,55 +120,111 @@ CREATE TABLE IF NOT EXISTS blocks (
119
120
PRIMARY KEY (reward_cycle, signer_signature_hash)
120
121
) STRICT" ;
121
122
122
- const CREATE_INDEXES : & str = "
123
+ static CREATE_INDEXES : & str = "
123
124
CREATE INDEX IF NOT EXISTS blocks_signed_over ON blocks (signed_over);
124
125
CREATE INDEX IF NOT EXISTS blocks_consensus_hash ON blocks (consensus_hash);
125
126
CREATE INDEX IF NOT EXISTS blocks_valid ON blocks ((json_extract(block_info, '$.valid')));
126
127
CREATE INDEX IF NOT EXISTS burn_blocks_height ON burn_blocks (block_height);
127
128
" ;
128
129
129
- const CREATE_SIGNER_STATE_TABLE : & str = "
130
+ static CREATE_SIGNER_STATE_TABLE : & str = "
130
131
CREATE TABLE IF NOT EXISTS signer_states (
131
132
reward_cycle INTEGER PRIMARY KEY,
132
133
encrypted_state BLOB NOT NULL
133
134
) STRICT" ;
134
135
135
- const CREATE_BURN_STATE_TABLE : & str = "
136
+ static CREATE_BURN_STATE_TABLE : & str = "
136
137
CREATE TABLE IF NOT EXISTS burn_blocks (
137
138
block_hash TEXT PRIMARY KEY,
138
139
block_height INTEGER NOT NULL,
139
140
received_time INTEGER NOT NULL
140
141
) STRICT" ;
141
142
143
+ static CREATE_DB_CONFIG : & str = "
144
+ CREATE TABLE db_config(
145
+ version INTEGER NOT NULL
146
+ ) STRICT
147
+ " ;
148
+
149
+ static DROP_SCHEMA_0 : & str = "
150
+ DROP TABLE IF EXISTS burn_blocks;
151
+ DROP TABLE IF EXISTS signer_states;
152
+ DROP TABLE IF EXISTS blocks;
153
+ DROP TABLE IF EXISTS db_config;" ;
154
+
155
+ static SCHEMA_1 : & [ & str ] = & [
156
+ DROP_SCHEMA_0 ,
157
+ CREATE_DB_CONFIG ,
158
+ CREATE_BURN_STATE_TABLE ,
159
+ CREATE_BLOCKS_TABLE ,
160
+ CREATE_SIGNER_STATE_TABLE ,
161
+ CREATE_INDEXES ,
162
+ "INSERT INTO db_config (version) VALUES (1);" ,
163
+ ] ;
164
+
142
165
impl SignerDb {
166
+ /// The current schema version used in this build of the signer binary.
167
+ pub const SCHEMA_VERSION : u32 = 1 ;
168
+
143
169
/// Create a new `SignerState` instance.
144
170
/// This will create a new SQLite database at the given path
145
171
/// or an in-memory database if the path is ":memory:"
146
172
pub fn new ( db_path : impl AsRef < Path > ) -> Result < Self , DBError > {
147
173
let connection = Self :: connect ( db_path) ?;
148
174
149
- let signer_db = Self { db : connection } ;
150
-
151
- signer_db. instantiate_db ( ) ?;
175
+ let mut signer_db = Self { db : connection } ;
176
+ signer_db. create_or_migrate ( ) ?;
152
177
153
178
Ok ( signer_db)
154
179
}
155
180
156
- fn instantiate_db ( & self ) -> Result < ( ) , DBError > {
157
- if !table_exists ( & self . db , "blocks" ) ? {
158
- self . db . execute ( CREATE_BLOCKS_TABLE , NO_PARAMS ) ?;
181
+ /// Returns the schema version of the database
182
+ fn get_schema_version ( conn : & Connection ) -> Result < u32 , DBError > {
183
+ if !table_exists ( conn, "db_config" ) ? {
184
+ return Ok ( 0 ) ;
159
185
}
186
+ let result = conn
187
+ . query_row ( "SELECT version FROM db_config LIMIT 1" , [ ] , |row| {
188
+ row. get ( 0 )
189
+ } )
190
+ . optional ( ) ;
191
+ match result {
192
+ Ok ( x) => Ok ( x. unwrap_or_else ( || 0 ) ) ,
193
+ Err ( e) => Err ( DBError :: from ( e) ) ,
194
+ }
195
+ }
160
196
161
- if !table_exists ( & self . db , "signer_states" ) ? {
162
- self . db . execute ( CREATE_SIGNER_STATE_TABLE , NO_PARAMS ) ?;
197
+ /// Migrate from schema 0 to schema 1
198
+ fn schema_1_migration ( tx : & Transaction ) -> Result < ( ) , DBError > {
199
+ if Self :: get_schema_version ( tx) ? >= 1 {
200
+ // no migration necessary
201
+ return Ok ( ( ) ) ;
163
202
}
164
203
165
- if ! table_exists ( & self . db , "burn_blocks" ) ? {
166
- self . db . execute ( CREATE_BURN_STATE_TABLE , NO_PARAMS ) ?;
204
+ for statement in SCHEMA_1 . iter ( ) {
205
+ tx . execute_batch ( statement ) ?;
167
206
}
168
207
169
- self . db . execute_batch ( CREATE_INDEXES ) ?;
208
+ Ok ( ( ) )
209
+ }
170
210
211
+ /// Either instantiate a new database, or migrate an existing one
212
+ /// If the detected version of the existing database is 0 (i.e., a pre-migration
213
+ /// logic DB, the DB will be dropped).
214
+ fn create_or_migrate ( & mut self ) -> Result < ( ) , DBError > {
215
+ let sql_tx = tx_begin_immediate ( & mut self . db ) ?;
216
+ loop {
217
+ let version = Self :: get_schema_version ( & sql_tx) ?;
218
+ match version {
219
+ 0 => Self :: schema_1_migration ( & sql_tx) ?,
220
+ 1 => break ,
221
+ x => return Err ( DBError :: Other ( format ! (
222
+ "Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}" ,
223
+ Self :: SCHEMA_VERSION ,
224
+ ) ) ) ,
225
+ }
226
+ }
227
+ sql_tx. commit ( ) ?;
171
228
Ok ( ( ) )
172
229
}
173
230
@@ -597,7 +654,7 @@ mod tests {
597
654
let db_path = tmp_db_path ( ) ;
598
655
let db = SignerDb :: new ( db_path) . expect ( "Failed to create signer db" ) ;
599
656
assert_eq ! (
600
- query_row( & db. db, "SELECT sqlite_version()" , NO_PARAMS ) . unwrap( ) ,
657
+ query_row( & db. db, "SELECT sqlite_version()" , [ ] ) . unwrap( ) ,
601
658
Some ( "3.45.0" . to_string( ) )
602
659
) ;
603
660
}
0 commit comments