• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MinaProtocol / mina / 2903

15 Nov 2024 01:59PM UTC coverage: 36.723% (-25.0%) from 61.682%
2903

Pull #16342

buildkite

dkijania
Merge branch 'dkijania/remove_publish_job_from_pr_comp' into dkijania/remove_publish_job_from_pr_dev
Pull Request #16342: [DEV] Publish debians only on nightly and stable

15 of 40 new or added lines in 14 files covered. (37.5%)

15175 existing lines in 340 files now uncovered.

24554 of 66863 relevant lines covered (36.72%)

20704.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.47
/src/lib/secrets/wallets.ml
1
open Core
5✔
2
open Async
3
module Secret_keypair = Keypair
4
open Signature_lib
5

6
(** The string is the filename of the secret key file *)
7
type locked_key =
8
  | Locked of string
9
  | Unlocked of (string * Keypair.t)
10
  | Hd_account of Mina_numbers.Hd_index.t
11

12
(* A simple cache on top of the fs *)
13
type t = { cache : locked_key Public_key.Compressed.Table.t; path : string }
14

15
let get_privkey_filename public_key =
UNCOV
16
  Public_key.Compressed.to_base58_check public_key
×
17

18
let get_path { path; cache } public_key =
19
  (* TODO: Do we need to version this? *)
UNCOV
20
  let filename =
×
UNCOV
21
    Public_key.Compressed.Table.find cache public_key
×
UNCOV
22
    |> Option.bind ~f:(function
×
23
         | Locked file | Unlocked (file, _) ->
×
24
             Option.return file
25
         | Hd_account _ ->
×
26
             Option.return
27
               (Public_key.Compressed.to_base58_check public_key ^ ".index") )
×
UNCOV
28
    |> Option.value ~default:(get_privkey_filename public_key)
×
29
  in
UNCOV
30
  path ^/ filename
×
31

32
let decode_public_key key file path logger =
UNCOV
33
  match
×
UNCOV
34
    Or_error.try_with (fun () -> Public_key.of_base58_check_decompress_exn key)
×
35
  with
UNCOV
36
  | Ok pk ->
×
37
      Some pk
38
  | Error e ->
×
39
      [%log error] "Error decoding public key at $path/$file: $error"
×
40
        ~metadata:
41
          [ ("file", `String file)
42
          ; ("path", `String path)
43
          ; ("error", Error_json.error_to_yojson e)
×
44
          ] ;
45
      None
×
46

47
let reload ~logger { cache; path } : unit Deferred.t =
UNCOV
48
  let logger =
×
49
    Logger.extend logger [ ("wallets_context", `String "Wallets.get") ]
50
  in
UNCOV
51
  Public_key.Compressed.Table.clear cache ;
×
UNCOV
52
  let%bind () = File_system.create_dir path in
×
UNCOV
53
  let%bind files = Sys.readdir path >>| Array.to_list in
×
54
  let%bind () =
UNCOV
55
    Deferred.List.iter files ~f:(fun file ->
×
UNCOV
56
        match String.chop_suffix file ~suffix:".pub" with
×
UNCOV
57
        | Some sk_filename -> (
×
UNCOV
58
            let%map lines = Reader.file_lines (path ^/ file) in
×
UNCOV
59
            match lines with
×
UNCOV
60
            | public_key :: _ ->
×
UNCOV
61
                decode_public_key public_key file path logger
×
62
                |> Option.iter ~f:(fun pk ->
UNCOV
63
                       ignore
×
UNCOV
64
                       @@ Public_key.Compressed.Table.add cache ~key:pk
×
65
                            ~data:(Locked sk_filename) )
66
            | _ ->
×
67
                () )
UNCOV
68
        | None -> (
×
69
            match String.chop_suffix file ~suffix:".index" with
70
            | Some public_key -> (
×
71
                let%map lines = Reader.file_lines (path ^/ file) in
×
72
                match lines with
×
73
                | hd_index :: _ ->
×
74
                    decode_public_key public_key file path logger
×
75
                    |> Option.iter ~f:(fun pk ->
76
                           ignore
×
77
                           @@ Public_key.Compressed.Table.add cache ~key:pk
×
78
                                ~data:
79
                                  (Hd_account
80
                                     (Mina_numbers.Hd_index.of_string hd_index)
×
81
                                  ) )
82
                | _ ->
×
83
                    () )
UNCOV
84
            | None ->
×
85
                return () ) )
86
  in
UNCOV
87
  Unix.chmod path ~perm:0o700
×
88

89
let load ~logger ~disk_location =
UNCOV
90
  let t =
×
UNCOV
91
    { cache = Public_key.Compressed.Table.create ()
×
UNCOV
92
    ; path = disk_location ^/ "store"
×
93
    }
94
  in
UNCOV
95
  let%map () = reload ~logger t in
×
UNCOV
96
  t
×
97

98
let import_keypair_helper t keypair write_keypair =
UNCOV
99
  let compressed_pk = Public_key.compress keypair.Keypair.public_key in
×
UNCOV
100
  let privkey_path = get_path t compressed_pk in
×
UNCOV
101
  let%bind () = write_keypair privkey_path in
×
UNCOV
102
  let%map () = Unix.chmod privkey_path ~perm:0o600 in
×
UNCOV
103
  ignore
×
UNCOV
104
    ( Public_key.Compressed.Table.add t.cache ~key:compressed_pk
×
UNCOV
105
        ~data:(Unlocked (get_privkey_filename compressed_pk, keypair))
×
106
      : [ `Duplicate | `Ok ] ) ;
107
  compressed_pk
108

109
let import_keypair t keypair ~password =
UNCOV
110
  import_keypair_helper t keypair (fun privkey_path ->
×
UNCOV
111
      Secret_keypair.write_exn keypair ~privkey_path ~password )
×
112

113
let import_keypair_terminal_stdin t keypair =
114
  import_keypair_helper t keypair (fun privkey_path ->
×
115
      Secret_keypair.Terminal_stdin.write_exn keypair ~privkey_path )
×
116

117
(** Generates a new private key file and a keypair *)
118
let generate_new t ~password : Public_key.Compressed.t Deferred.t =
UNCOV
119
  let keypair = Keypair.create () in
×
UNCOV
120
  import_keypair t keypair ~password
×
121

122
let create_hd_account t ~hd_index :
123
    (Public_key.Compressed.t, string) Deferred.Result.t =
124
  let open Deferred.Result.Let_syntax in
×
125
  let%bind public_key = Hardware_wallets.compute_public_key ~hd_index in
126
  let compressed_pk = Public_key.compress public_key in
×
127
  let index_path =
×
128
    t.path ^/ Public_key.Compressed.to_base58_check compressed_pk ^ ".index"
×
129
  in
130
  let%bind () =
131
    Hardware_wallets.write_exn ~hd_index ~index_path
132
    |> Deferred.map ~f:Result.return
×
133
  in
134
  let%map () =
135
    Unix.chmod index_path ~perm:0o600 |> Deferred.map ~f:Result.return
×
136
  in
137
  ignore
×
138
    ( Public_key.Compressed.Table.add t.cache ~key:compressed_pk
×
139
        ~data:(Hd_account hd_index)
140
      : [ `Duplicate | `Ok ] ) ;
141
  compressed_pk
142

143
let delete ({ cache; _ } as t : t) (pk : Public_key.Compressed.t) :
144
    (unit, [ `Not_found ]) Deferred.Result.t =
UNCOV
145
  Hashtbl.remove cache pk ;
×
UNCOV
146
  Deferred.Or_error.try_with ~here:[%here] (fun () ->
×
UNCOV
147
      Unix.remove (get_path t pk) )
×
UNCOV
148
  |> Deferred.Result.map_error ~f:(fun _ -> `Not_found)
×
149

UNCOV
150
let pks ({ cache; _ } : t) = Public_key.Compressed.Table.keys cache
×
151

152
let find_unlocked ({ cache; _ } : t) ~needle =
UNCOV
153
  Public_key.Compressed.Table.find cache needle
×
154
  |> Option.bind ~f:(function
155
       | Locked _ ->
×
156
           None
UNCOV
157
       | Unlocked (_, kp) ->
×
158
           Some kp
159
       | Hd_account _ ->
×
160
           None )
161

162
let find_identity ({ cache; _ } : t) ~needle =
163
  Public_key.Compressed.Table.find cache needle
×
164
  |> Option.bind ~f:(function
165
       | Locked _ ->
×
166
           None
167
       | Unlocked (_, kp) ->
×
168
           Some (`Keypair kp)
169
       | Hd_account index ->
×
170
           Some (`Hd_index index) )
171

172
let check_locked { cache; _ } ~needle =
173
  Public_key.Compressed.Table.find cache needle
×
174
  |> Option.map ~f:(function
175
       | Locked _ ->
×
176
           true
177
       | Unlocked _ ->
×
178
           false
179
       | Hd_account _ ->
×
180
           true )
181

182
let unlock { cache; path } ~needle ~password =
183
  let unlock_keypair = function
×
184
    | Locked file ->
×
185
        Secret_keypair.read ~privkey_path:(path ^/ file) ~password
×
186
        |> Deferred.Result.map_error ~f:(fun e -> `Key_read_error e)
×
187
        |> Deferred.Result.map ~f:(fun kp ->
×
188
               Public_key.Compressed.Table.set cache ~key:needle
×
189
                 ~data:(Unlocked (file, kp)) )
190
        |> Deferred.Result.ignore_m
191
    | Unlocked _ ->
×
192
        Deferred.Result.return ()
193
    | Hd_account _ ->
×
194
        Deferred.Result.return ()
195
  in
196
  Public_key.Compressed.Table.find cache needle
×
197
  |> Result.of_option ~error:`Not_found
×
198
  |> Deferred.return
×
199
  |> Deferred.Result.bind ~f:unlock_keypair
200

201
let lock { cache; _ } ~needle =
202
  Public_key.Compressed.Table.change cache needle ~f:(function
×
203
    | Some (Unlocked (file, _)) ->
×
204
        Some (Locked file)
205
    | k ->
×
206
        k )
207

208
let get_tracked_keypair ~logger ~which ~read_from_env_exn ~conf_dir pk =
209
  let%bind wallets = load ~logger ~disk_location:(conf_dir ^/ "wallets") in
×
210
  let sk_file = get_path wallets pk in
×
211
  read_from_env_exn ~which sk_file
×
212

213
let%test_module "wallets" =
214
  ( module struct
UNCOV
215
    let logger = Logger.create ()
×
216

UNCOV
217
    let password = lazy (Deferred.return (Bytes.of_string ""))
×
218

219
    module Set = Public_key.Compressed.Set
220

221
    let%test_unit "get from scratch" =
UNCOV
222
      Async.Thread_safe.block_on_async_exn (fun () ->
×
UNCOV
223
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
224
              let%bind wallets = load ~logger ~disk_location:path in
UNCOV
225
              let%map pk = generate_new wallets ~password in
×
UNCOV
226
              let keys = Set.of_list (pks wallets) in
×
UNCOV
227
              assert (Set.mem keys pk) ;
×
UNCOV
228
              assert (find_unlocked wallets ~needle:pk |> Option.is_some) ) )
×
229

230
    let%test_unit "get from existing file system not-scratch" =
UNCOV
231
      Backtrace.elide := false ;
×
232
      Async.Thread_safe.block_on_async_exn (fun () ->
UNCOV
233
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
234
              let%bind wallets = load ~logger ~disk_location:path in
UNCOV
235
              let%bind pk1 = generate_new wallets ~password in
×
UNCOV
236
              let%bind pk2 = generate_new wallets ~password in
×
UNCOV
237
              let keys = Set.of_list (pks wallets) in
×
UNCOV
238
              assert (Set.mem keys pk1 && Set.mem keys pk2) ;
×
239
              (* Get wallets again from scratch *)
240
              let%map wallets = load ~logger ~disk_location:path in
UNCOV
241
              let keys = Set.of_list (pks wallets) in
×
UNCOV
242
              assert (Set.mem keys pk1 && Set.mem keys pk2) ) )
×
243

244
    let%test_unit "create wallet then delete it" =
UNCOV
245
      Async.Thread_safe.block_on_async_exn (fun () ->
×
UNCOV
246
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
247
              let%bind wallets = load ~logger ~disk_location:path in
UNCOV
248
              let%bind pk = generate_new wallets ~password in
×
UNCOV
249
              let keys = Set.of_list (pks wallets) in
×
UNCOV
250
              assert (Set.mem keys pk) ;
×
UNCOV
251
              match%map delete wallets pk with
×
UNCOV
252
              | Ok () ->
×
UNCOV
253
                  assert (
×
UNCOV
254
                    Option.is_none
×
UNCOV
255
                    @@ Public_key.Compressed.Table.find wallets.cache pk )
×
256
              | Error _ ->
×
257
                  failwith "unexpected" ) )
258

259
    let%test_unit "Unable to find wallet" =
UNCOV
260
      Async.Thread_safe.block_on_async_exn (fun () ->
×
UNCOV
261
          File_system.with_temp_dir "/tmp/coda-wallets-test" ~f:(fun path ->
×
262
              let%bind wallets = load ~logger ~disk_location:path in
UNCOV
263
              let keypair = Keypair.create () in
×
264
              let%map result =
UNCOV
265
                delete wallets (Public_key.compress @@ keypair.public_key)
×
266
              in
UNCOV
267
              assert (Result.is_error result) ) )
×
268
  end )
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc