Skip to content

Commit f03a532

Browse files
committed
Make converting ledger sync check more defensive
If a daemon is stopped at certain points while syncing a converting ledger to a network, the underlying databases can be left in an unexpected state: the num_accounts in the database will be set to a high value (because the daemon added a batch of accounts at high addresses) but not all accounts at lower indices will be present (because the daemon didn't get to filling them in yet). This will cause functions like iteri and get_at_index_exn to throw unexpected exceptions when they get to addresses that are missing accounts. A couple of new ledger functions have been added to account for this possibility, which are used in an updated ledger database sync check implementation. A few related unit tests have been added to the converting merkle tree, one of which does not pass without these changes being in place.
1 parent 5da42cc commit f03a532

File tree

7 files changed

+100
-8
lines changed

7 files changed

+100
-8
lines changed

src/lib/merkle_ledger/any_ledger.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ module Make_base (Inputs : Intf.Inputs.Intf) :
8181

8282
let set_at_index_exn (T ((module Base), t)) = Base.set_at_index_exn t
8383

84+
let get_at_index (T ((module Base), t)) = Base.get_at_index t
85+
8486
let get_at_index_exn (T ((module Base), t)) = Base.get_at_index_exn t
8587

8688
let set_batch ?hash_cache (T ((module Base), t)) =
@@ -118,6 +120,8 @@ module Make_base (Inputs : Intf.Inputs.Intf) :
118120

119121
let token_owners (T ((module Base), t)) = Base.token_owners t
120122

123+
let iteri_untrusted (T ((module Base), t)) = Base.iteri_untrusted t
124+
121125
let iteri (T ((module Base), t)) = Base.iteri t
122126

123127
(* ignored_keys must be Base.Keys.Set.t, but that isn't necessarily the same as Keys.Set.t for the

src/lib/merkle_ledger/converting_merkle_tree.ml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ end)
109109

110110
let to_list_sequential t = Primary_ledger.to_list_sequential t.primary_ledger
111111

112+
let iteri_untrusted t ~f = Primary_ledger.iteri_untrusted t.primary_ledger ~f
113+
112114
let iteri t ~f = Primary_ledger.iteri t.primary_ledger ~f
113115

114116
let foldi t ~init ~f = Primary_ledger.foldi t.primary_ledger ~init ~f
@@ -180,6 +182,8 @@ end)
180182
~f:(fun (loc, account) -> (loc, convert account))
181183
located_accounts )
182184

185+
let get_at_index t idx = Primary_ledger.get_at_index t.primary_ledger idx
186+
183187
let get_at_index_exn t idx =
184188
Primary_ledger.get_at_index_exn t.primary_ledger idx
185189

@@ -264,12 +268,12 @@ struct
264268
Primary_db.num_accounts db1 = Converting_db.num_accounts db2
265269
&&
266270
let is_synced = ref true in
267-
Primary_db.iteri db1 ~f:(fun idx stable_account ->
268-
let expected_unstable_account = convert stable_account in
269-
let actual_unstable_account = Converting_db.get_at_index_exn db2 idx in
271+
Primary_db.iteri_untrusted db1 ~f:(fun idx stable_account ->
272+
let expected_unstable_account = Option.map ~f:convert stable_account in
273+
let actual_unstable_account = Converting_db.get_at_index db2 idx in
270274
if
271275
not
272-
(Inputs.converted_equal expected_unstable_account
276+
(Option.equal Inputs.converted_equal expected_unstable_account
273277
actual_unstable_account )
274278
then is_synced := false ) ;
275279
!is_synced

src/lib/merkle_ledger/database.ml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,11 @@ module Make (Inputs : Intf.Inputs.DATABASE) = struct
278278
Option.map (last_location mdb) ~f:Location.to_path_exn
279279
end
280280

281-
let get_at_index_exn mdb index =
281+
let get_at_index mdb index =
282282
let addr = Addr.of_int_exn ~ledger_depth:mdb.depth index in
283-
get mdb (Location.Account addr) |> Option.value_exn
283+
get mdb (Location.Account addr)
284+
285+
let get_at_index_exn mdb index = get_at_index mdb index |> Option.value_exn
284286

285287
let all_accounts (t : t) =
286288
match Account_location.last_location_address t with
@@ -572,6 +574,14 @@ module Make (Inputs : Intf.Inputs.DATABASE) = struct
572574
| Ok location ->
573575
Ok (`Existed, location)
574576

577+
let iteri_untrusted t ~f =
578+
match Account_location.last_location_address t with
579+
| None ->
580+
()
581+
| Some last_addr ->
582+
Sequence.range ~stop:`inclusive 0 (Addr.to_int last_addr)
583+
|> Sequence.iter ~f:(fun i -> f i (get_at_index t i))
584+
575585
let iteri t ~f =
576586
match Account_location.last_location_address t with
577587
| None ->

src/lib/merkle_ledger/intf.ml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ module Ledger = struct
320320
(** list of accounts via slower sequential mechanism *)
321321
val to_list_sequential : t -> account list
322322

323+
(** iterate over all indexes and accounts, if the ledger is not known to be sound *)
324+
val iteri_untrusted : t -> f:(index -> account option -> unit) -> unit
325+
323326
(** iterate over all indexes and accounts *)
324327
val iteri : t -> f:(index -> account -> unit) -> unit
325328

@@ -391,6 +394,8 @@ module Ledger = struct
391394
val set_batch :
392395
?hash_cache:hash Addr.Map.t -> t -> (Location.t * account) list -> unit
393396

397+
val get_at_index : t -> int -> account option
398+
394399
val get_at_index_exn : t -> int -> account
395400

396401
val set_at_index_exn : t -> int -> account -> unit
@@ -548,6 +553,8 @@ module Ledger = struct
548553

549554
module Config : Config
550555

556+
val dbs_synced : primary_ledger -> converting_ledger -> bool
557+
551558
(** Create a new converting merkle tree with the given configuration. If
552559
[In_directories] is given, existing databases will be opened and used to
553560
back the converting merkle tree. If the converting database does not exist

src/lib/merkle_ledger/null_ledger.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ end = struct
9595
let set_at_index_exn _t =
9696
failwith "set_at_index_exn: null ledgers cannot be mutated"
9797

98+
let get_at_index _t _index = None
99+
98100
let get_at_index_exn _t = failwith "get_at_index_exn: null ledgers are empty"
99101

100102
let set_batch ?hash_cache:_ _t =
@@ -130,6 +132,8 @@ end = struct
130132

131133
let tokens _t _pk = Token_id.Set.empty
132134

135+
let iteri_untrusted _t ~f:_ = ()
136+
133137
let iteri _t ~f:_ = ()
134138

135139
let fold_until _t ~init ~f:_ ~finish = Async.Deferred.return @@ finish init

src/lib/merkle_ledger/test/test_converting.ml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,59 @@ struct
207207
[%test_eq: Migrated.Account.t] stored_migrated_account
208208
(Db_converting.convert primary_account) ) ) ) )
209209

210+
let () =
211+
add_test
212+
"sync detection fails without crashing after accounts are added at high \
213+
addresses" (fun () ->
214+
with_primary ~f:(fun primary ->
215+
let depth = Db.depth primary in
216+
let max_height = Int.min 5 depth - 1 in
217+
populate_primary_db primary max_height ;
218+
with_migrated ~f:(fun migrated ->
219+
let _converting =
220+
Db_converting.of_ledgers_with_migration primary migrated
221+
in
222+
let additional_account = Quickcheck.random_value Account.gen in
223+
let high_index = (1 lsl Int.min 5 depth) - 1 in
224+
let additional_account_addr =
225+
Db.Addr.of_int_exn ~ledger_depth:depth high_index
226+
in
227+
(* Using set_batch_accounts with a high address like this leaves
228+
the databases in an inconsistent state, because it updates
229+
the last added account in the databases but doesn't fill in
230+
the accounts at lower addresses. This state is similar to
231+
what you might get after an incomplete ledger sync. *)
232+
Db.set_batch_accounts primary
233+
[ (additional_account_addr, additional_account) ] ;
234+
Db_migrated.set_batch_accounts migrated
235+
[ ( additional_account_addr
236+
, Db_converting.convert additional_account )
237+
] ;
238+
assert (Db_converting.dbs_synced primary migrated) ) ) )
239+
240+
let () =
241+
add_test "sync detection fails after converting ledger account is mutated"
242+
(fun () ->
243+
with_primary ~f:(fun primary ->
244+
let depth = Db.depth primary in
245+
let max_height = Int.min 5 depth in
246+
populate_primary_db primary max_height ;
247+
let account_to_mutate = Db.get_at_index_exn primary 0 in
248+
let new_balance, _overflow_flag =
249+
Balance.add_amount_flagged account_to_mutate.balance
250+
Currency.Amount.one
251+
in
252+
let mutated_account =
253+
Db_converting.convert
254+
{ account_to_mutate with balance = new_balance }
255+
in
256+
with_migrated ~f:(fun migrated ->
257+
let _converting =
258+
Db_converting.of_ledgers_with_migration primary migrated
259+
in
260+
Db_migrated.set_at_index_exn migrated 0 mutated_account ;
261+
assert (not (Db_converting.dbs_synced primary migrated)) ) ) )
262+
210263
let () =
211264
add_test "create converting ledger, populate randomly, test iteration order"
212265
(fun () ->

src/lib/merkle_mask/masking_merkle_tree.ml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -879,10 +879,14 @@ module Make (Inputs : Inputs_intf.S) = struct
879879
let addr = Location.to_path_exn location in
880880
Addr.to_int addr
881881

882-
let get_at_index_exn t index =
882+
let get_at_index t index =
883883
assert_is_attached t ;
884884
let addr = Addr.of_int_exn ~ledger_depth:t.depth index in
885-
get t (Location.Account addr) |> Option.value_exn
885+
get t (Location.Account addr)
886+
887+
let get_at_index_exn t index =
888+
assert_is_attached t ;
889+
get_at_index t index |> Option.value_exn
886890

887891
let set_at_index_exn t index account =
888892
assert_is_attached t ;
@@ -906,6 +910,12 @@ module Make (Inputs : Inputs_intf.S) = struct
906910
let%map.Async.Deferred accts = to_list t in
907911
List.map accts ~f:Account.identifier |> Account_id.Set.of_list
908912

913+
let iteri_untrusted t ~f =
914+
assert_is_attached t ;
915+
let num_accounts = num_accounts t in
916+
Sequence.range ~stop:`exclusive 0 num_accounts
917+
|> Sequence.iter ~f:(fun i -> f i (get_at_index t i))
918+
909919
let iteri t ~f =
910920
assert_is_attached t ;
911921
let num_accounts = num_accounts t in

0 commit comments

Comments
 (0)