skeleton_key 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +542 -0
  3. data/bin/console +8 -0
  4. data/bin/lint +10 -0
  5. data/bin/setup +21 -0
  6. data/lib/skeleton_key/chains/bitcoin/account.rb +101 -0
  7. data/lib/skeleton_key/chains/bitcoin/account_derivation.rb +127 -0
  8. data/lib/skeleton_key/chains/bitcoin/support/outputs.rb +77 -0
  9. data/lib/skeleton_key/chains/bitcoin/support/paths.rb +34 -0
  10. data/lib/skeleton_key/chains/bitcoin/support/versioning.rb +87 -0
  11. data/lib/skeleton_key/chains/bitcoin/support.rb +48 -0
  12. data/lib/skeleton_key/chains/ethereum/account.rb +191 -0
  13. data/lib/skeleton_key/chains/ethereum/support.rb +143 -0
  14. data/lib/skeleton_key/chains/solana/account.rb +117 -0
  15. data/lib/skeleton_key/chains/solana/support.rb +27 -0
  16. data/lib/skeleton_key/codecs/base58.rb +64 -0
  17. data/lib/skeleton_key/codecs/base58_check.rb +42 -0
  18. data/lib/skeleton_key/codecs/bech32.rb +182 -0
  19. data/lib/skeleton_key/constants.rb +68 -0
  20. data/lib/skeleton_key/core/entropy.rb +37 -0
  21. data/lib/skeleton_key/derivation/bip32.rb +182 -0
  22. data/lib/skeleton_key/derivation/path.rb +112 -0
  23. data/lib/skeleton_key/derivation/slip10.rb +89 -0
  24. data/lib/skeleton_key/errors.rb +158 -0
  25. data/lib/skeleton_key/keyring.rb +63 -0
  26. data/lib/skeleton_key/recovery/bip39.rb +212 -0
  27. data/lib/skeleton_key/recovery/bip39_english.txt +2048 -0
  28. data/lib/skeleton_key/recovery/slip39.rb +220 -0
  29. data/lib/skeleton_key/recovery/slip39_support/bit_packing.rb +37 -0
  30. data/lib/skeleton_key/recovery/slip39_support/checksum.rb +53 -0
  31. data/lib/skeleton_key/recovery/slip39_support/cipher.rb +81 -0
  32. data/lib/skeleton_key/recovery/slip39_support/decoder.rb +109 -0
  33. data/lib/skeleton_key/recovery/slip39_support/encoder.rb +48 -0
  34. data/lib/skeleton_key/recovery/slip39_support/generated_set.rb +39 -0
  35. data/lib/skeleton_key/recovery/slip39_support/generator.rb +156 -0
  36. data/lib/skeleton_key/recovery/slip39_support/interpolation.rb +71 -0
  37. data/lib/skeleton_key/recovery/slip39_support/protocol.rb +34 -0
  38. data/lib/skeleton_key/recovery/slip39_support/secret_recovery.rb +74 -0
  39. data/lib/skeleton_key/recovery/slip39_support/share.rb +50 -0
  40. data/lib/skeleton_key/recovery/slip39_wordlist.txt +1024 -0
  41. data/lib/skeleton_key/seed.rb +127 -0
  42. data/lib/skeleton_key/skeleton_key.code-workspace +11 -0
  43. data/lib/skeleton_key/utils/encoding.rb +134 -0
  44. data/lib/skeleton_key/utils/hashing.rb +238 -0
  45. data/lib/skeleton_key/version.rb +8 -0
  46. data/lib/skeleton_key.rb +66 -0
  47. metadata +107 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f7a0cbc245df26ea9b06a09ea21cc92336a5539853e235b33df9b49e65ab738
4
+ data.tar.gz: 055e8323ee4457a6704d422f110d6a7140b4541e5be1a951591c7d78e74b7134
5
+ SHA512:
6
+ metadata.gz: 6baf89deae1dce76f2552a1133d3d42638e2604783f782c46f83357f49cad785742a859affdfdeaa393f721ffa9502fd413fe53937288d9deaf6a23d39a3f848
7
+ data.tar.gz: cb22f9883391cbc02a3589101ff8830bcd8beed1ea78f83b4094cb9ab002310414e06571da5ada1b5151d0e730708bc3ba888eccc399ad816e0627d6a6c2e080
data/README.md ADDED
@@ -0,0 +1,542 @@
1
+ # SkeletonKey
2
+
3
+ ```
4
+ ██████████████
5
+ ██████▓░░░░░░░░░▓██████
6
+ ███░░░░▓████████████▓░░░░███
7
+ ██░░▓██▓░░░██░░░██░░░▓██▓░░██
8
+ ██░██░░██░░░██░░░██░░░██░░██░██
9
+ ██░██░░██░░░██░░░██░░░██░░██░██
10
+ ██░░▓██▓░░░██░░░██░░░▓██▓░░██
11
+ ███░░░░▓████████████▓░░░░███
12
+ ██████▓░░░░░░░░░▓██████
13
+ ████████████████
14
+ ██░░██
15
+ ██░░██ ╔═╗╦╔═╔═╗╦ ╔═╗╔╦╗╔═╗╔╗╔
16
+ ██░░██ ╚═╗╠╩╗║╣ ║ ║╣ ║ ║ ║║║║
17
+ ██░░██ ╚═╝╩ ╩╚═╝╩═╝╚═╝ ╩ ╚═╝╝╚╝
18
+ ██░░██ ╦╔═╔═╗╦ ╦
19
+ ██░░██ ╠╩╗║╣ ╚╦╝
20
+ ██░░██ ╩ ╩╚═╝ ╩
21
+ ██░░██
22
+ ██░░████ Zero-dependency deterministic wallet
23
+ ██░░░░██ recovery & key derivation for
24
+ ██░░████
25
+ ██░░██
26
+ ██░░██████
27
+ ██░░░░░░██
28
+ ██░░██████
29
+ ██░░██
30
+ ██████
31
+ ```
32
+
33
+ [![Gem Version](https://badge.fury.io/rb/skeleton_key.svg )](https://badge.fury.io/rb/skeleton_key )
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg )](LICENSE)
35
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.2-black )](https://www.ruby-lang.org )
36
+ [![Safety-Critical](https://img.shields.io/badge/⚠%20Safety--Critical-black )](SECURITY.md)
37
+
38
+ SkeletonKey is a Ruby library for deterministic wallet recovery and key derivation across Bitcoin, Ethereum, and Solana. It is designed around a strict boundary:
39
+
40
+ - recovery formats in the recovery layer
41
+ - shared seed and derivation primitives in the shared layer
42
+ - chain-specific address and serialization behavior in chain modules
43
+
44
+ This repository is safety-critical. A small bug in recovery, derivation, encoding, or serialization can produce valid-looking but wrong keys.
45
+
46
+ ## What SkeletonKey Does
47
+
48
+ SkeletonKey takes a root secret and turns it into chain-specific accounts and addresses:
49
+
50
+ - generate a new BIP39 mnemonic
51
+ - recover a seed from a BIP39 mnemonic
52
+ - generate new SLIP-0039 shares
53
+ - recover a master secret from SLIP-0039 shares
54
+ - normalize raw seeds into a canonical `SkeletonKey::Seed`
55
+ - derive Bitcoin, Ethereum, and Solana accounts from one seed
56
+ - validate behavior against large golden-master fixture sets
57
+
58
+ Current supported standards and conventions:
59
+
60
+ - BIP39 recovery
61
+ - SLIP-0039 recovery
62
+ - BIP32 secp256k1 derivation
63
+ - SLIP-0010 Ed25519 derivation
64
+ - Bitcoin BIP32, BIP44, BIP49, BIP84, BIP141
65
+ - Ethereum BIP32 and BIP44
66
+ - Solana hardened BIP44-style paths
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ bin/setup
72
+ ```
73
+
74
+ Or, if the environment is already prepared:
75
+
76
+ ```bash
77
+ bundle install
78
+ ```
79
+
80
+ ## Developer Quick Start
81
+
82
+ Open a console with the library loaded:
83
+
84
+ ```bash
85
+ bin/console
86
+ ```
87
+
88
+ Generate a random keyring:
89
+
90
+ ```ruby
91
+ keyring = SkeletonKey::Keyring.new
92
+ ```
93
+
94
+ Initialize from an existing hex seed:
95
+
96
+ ```ruby
97
+ keyring = SkeletonKey::Keyring.new(
98
+ seed: "13e3e43b779fc6cda3bd9a1e762768dd3e273389adb81787adbe880341609e88"
99
+ )
100
+ ```
101
+
102
+ Derive default chain accounts:
103
+
104
+ ```ruby
105
+ bitcoin = keyring.bitcoin
106
+ ethereum = keyring.ethereum
107
+ solana = keyring.solana
108
+ ```
109
+
110
+ ## Recovery Experience
111
+
112
+ ### BIP39
113
+
114
+ Generate a new mnemonic:
115
+
116
+ ```ruby
117
+ mnemonic = SkeletonKey::Recovery::Bip39.generate(word_count: 24)
118
+
119
+ mnemonic.phrase
120
+ mnemonic.words
121
+ mnemonic.seed
122
+ ```
123
+
124
+ Generate deterministically from explicit entropy:
125
+
126
+ ```ruby
127
+ mnemonic = SkeletonKey::Recovery::Bip39.generate(
128
+ word_count: 12,
129
+ entropy: ("\x00".b * 16)
130
+ )
131
+ ```
132
+
133
+ Or convert entropy directly:
134
+
135
+ ```ruby
136
+ mnemonic = SkeletonKey::Recovery::Bip39.from_entropy("00000000000000000000000000000000")
137
+ ```
138
+
139
+ Use [`SkeletonKey::Recovery::Bip39`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/recovery/bip39.rb) when you want explicit mnemonic validation and seed recovery:
140
+
141
+ ```ruby
142
+ bip39 = SkeletonKey::Recovery::Bip39.new(
143
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
144
+ )
145
+
146
+ seed = bip39.seed
147
+ keyring = SkeletonKey::Keyring.new(seed: seed)
148
+ ```
149
+
150
+ You can also pass a mnemonic directly to `Seed.import` or `Keyring.new`:
151
+
152
+ ```ruby
153
+ seed = SkeletonKey::Seed.import(
154
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
155
+ )
156
+
157
+ keyring = SkeletonKey::Keyring.new(
158
+ seed: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
159
+ )
160
+ ```
161
+
162
+ What BIP39 validation currently enforces:
163
+
164
+ - official BIP39 word counts only: `12`, `15`, `18`, `21`, `24`
165
+ - English wordlist membership
166
+ - checksum validation
167
+ - PBKDF2 seed reconstruction
168
+
169
+ ### SLIP-0039
170
+
171
+ Generate a single-group SLIP-0039 share set:
172
+
173
+ ```ruby
174
+ share_set = SkeletonKey::Recovery::Slip39.generate(
175
+ master_secret: "00112233445566778899aabbccddeeff",
176
+ member_threshold: 3,
177
+ member_count: 5,
178
+ passphrase: "",
179
+ extendable: true,
180
+ iteration_exponent: 1
181
+ )
182
+
183
+ share_set.mnemonic_groups
184
+ share_set.all_shares
185
+ share_set.recovery_set
186
+ ```
187
+
188
+ Generate a multi-group share set:
189
+
190
+ ```ruby
191
+ share_set = SkeletonKey::Recovery::Slip39.generate(
192
+ master_secret: "00112233445566778899aabbccddeeff",
193
+ group_threshold: 2,
194
+ groups: [
195
+ { member_threshold: 2, member_count: 3 },
196
+ { member_threshold: 3, member_count: 5 },
197
+ { member_threshold: 2, member_count: 4 }
198
+ ],
199
+ passphrase: "PASS8",
200
+ extendable: false,
201
+ iteration_exponent: 2
202
+ )
203
+ ```
204
+
205
+ `master_secret` may be raw bytes, hex, octets, or a `SkeletonKey::Seed`, but SLIP-0039 generation only accepts master-secret lengths of `16`, `24`, or `32` bytes.
206
+
207
+ Use [`SkeletonKey::Recovery::Slip39`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/recovery/slip39.rb) when recovering from Shamir shares:
208
+
209
+ ```ruby
210
+ shares = [
211
+ "share one ...",
212
+ "share two ...",
213
+ "share three ..."
214
+ ]
215
+
216
+ seed = SkeletonKey::Recovery::Slip39.recover(shares, passphrase: "")
217
+ keyring = SkeletonKey::Keyring.new(seed: seed)
218
+ ```
219
+
220
+ Important DX rule: pass a **flat array of share strings**. Do not group them yourself. Group membership is inferred from the metadata encoded inside each share.
221
+
222
+ Multi-group recovery uses the same interface:
223
+
224
+ ```ruby
225
+ shares = [
226
+ group_0_share_0,
227
+ group_0_share_1,
228
+ group_2_share_0,
229
+ group_2_share_1
230
+ ]
231
+
232
+ seed = SkeletonKey::Recovery::Slip39.recover(shares, passphrase: "PASS8")
233
+ ```
234
+
235
+ Important safety note: `Slip39.recover` validates the share set, but a wrong passphrase is not guaranteed to raise. It can yield a different valid-length secret. If passphrase correctness matters operationally, verify the recovered seed against a known address, fingerprint, or other expected identifier.
236
+
237
+ ## Ruby Secret Handling
238
+
239
+ SkeletonKey is intentionally a sharp library: the point is to recover and export key material so the caller can hand it to signing, wallet, or custody code. That is useful, but it carries Ruby-specific constraints.
240
+
241
+ Ruby does not provide hard guarantees for secure memory erasure:
242
+
243
+ - strings are garbage-collected
244
+ - sensitive values may be copied during encoding, packing, or concatenation
245
+ - intermediate buffers may exist outside the object you hold
246
+
247
+ SkeletonKey therefore does not claim guaranteed zeroization. The correct posture is best-effort operational hygiene:
248
+
249
+ - keep mnemonics, seeds, private keys, and WIFs in scope for as little time as possible
250
+ - avoid logging, inspecting, or serializing secret-bearing objects in development tools
251
+ - prefer process boundaries and short-lived workers for sensitive workflows
252
+ - hand recovered keys directly to downstream signing code instead of caching them in application state
253
+ - verify recovered BIP39 or SLIP-0039 material against known addresses or fingerprints before using it operationally
254
+
255
+ If your threat model requires hard memory guarantees, Ruby is the wrong layer to trust with long-lived secret custody.
256
+
257
+ ## Keyring Experience
258
+
259
+ [`SkeletonKey::Keyring`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/keyring.rb) is the main developer entry point. It accepts normalized seed material and exposes chain-specific account builders.
260
+
261
+ ```ruby
262
+ keyring = SkeletonKey::Keyring.new(seed: seed)
263
+
264
+ btc = keyring.bitcoin(purpose: 84, coin_type: 0, account_index: 0, network: :mainnet)
265
+ eth = keyring.ethereum(purpose: 44, coin_type: 60, account_index: 0)
266
+ sol = keyring.solana(account_index: 0)
267
+ ```
268
+
269
+ Supported seed input shapes:
270
+
271
+ - `nil` for a new random seed
272
+ - `SkeletonKey::Seed`
273
+ - raw bytes
274
+ - hex seed string
275
+ - array of octets
276
+ - BIP39 mnemonic string
277
+
278
+ ### End-to-End Example
279
+
280
+ This is the simplest full-circle flow for manual testing in `bin/console`:
281
+
282
+ ```ruby
283
+ mnemonic = SkeletonKey::Recovery::Bip39.generate(word_count: 12)
284
+
285
+ puts mnemonic.phrase
286
+ puts mnemonic.seed.hex
287
+
288
+ keyring = SkeletonKey::Keyring.new(seed: mnemonic.seed)
289
+
290
+ bitcoin = keyring.bitcoin(purpose: 84, coin_type: 0, account_index: 0, network: :mainnet)
291
+ ethereum = keyring.ethereum(purpose: 44, coin_type: 60, account_index: 0)
292
+ solana = keyring.solana(account_index: 0)
293
+
294
+ btc_node = bitcoin.address(change: 0, index: 0)
295
+ eth_node = ethereum.address(change: 0, index: 0)
296
+ sol_node = solana.address(change: 0)
297
+
298
+ puts btc_node[:path]
299
+ puts btc_node[:address]
300
+ puts btc_node[:wif]
301
+
302
+ puts eth_node[:path]
303
+ puts eth_node[:address]
304
+ puts eth_node[:private_key]
305
+
306
+ puts sol_node[:path]
307
+ puts sol_node[:address]
308
+ puts sol_node[:private_key]
309
+ ```
310
+
311
+ If you want to verify that the mnemonic alone is sufficient, reconstruct the same keyring from the phrase:
312
+
313
+ ```ruby
314
+ phrase = mnemonic.phrase
315
+
316
+ recovered = SkeletonKey::Keyring.new(seed: phrase)
317
+ recovered_btc = recovered.bitcoin(purpose: 84, coin_type: 0, account_index: 0, network: :mainnet)
318
+ recovered_eth = recovered.ethereum(purpose: 44, coin_type: 60, account_index: 0)
319
+ recovered_sol = recovered.solana(account_index: 0)
320
+
321
+ expect_btc = recovered_btc.address(change: 0, index: 0)
322
+ expect_eth = recovered_eth.address(change: 0, index: 0)
323
+ expect_sol = recovered_sol.address(change: 0)
324
+
325
+ puts expect_btc[:address] == btc_node[:address]
326
+ puts expect_eth[:address] == eth_node[:address]
327
+ puts expect_sol[:address] == sol_node[:address]
328
+ ```
329
+
330
+ All three comparisons should print `true`.
331
+
332
+ ## Bitcoin Experience
333
+
334
+ Bitcoin accounts are created through [`SkeletonKey::Chains::Bitcoin::Account`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/chains/bitcoin/account.rb).
335
+
336
+ Examples:
337
+
338
+ ```ruby
339
+ account = keyring.bitcoin(purpose: 84, coin_type: 0, account_index: 0, network: :mainnet)
340
+
341
+ account.xprv
342
+ account.xpub
343
+ account.path
344
+ ```
345
+
346
+ Derive an address:
347
+
348
+ ```ruby
349
+ node = account.address(change: 0, index: 0)
350
+
351
+ node[:path]
352
+ node[:address]
353
+ node[:wif]
354
+ node[:privkey]
355
+ node[:pubkey]
356
+ ```
357
+
358
+ Derive a branch extended keypair:
359
+
360
+ ```ruby
361
+ branch = account.branch_extended_keys(change: 0)
362
+
363
+ branch[:path]
364
+ branch[:xprv]
365
+ branch[:xpub]
366
+ ```
367
+
368
+ Supported Bitcoin purposes:
369
+
370
+ - `32`: legacy root-branch BIP32 vectors
371
+ - `44`: BIP44 P2PKH
372
+ - `49`: BIP49 wrapped SegWit
373
+ - `84`: BIP84 native SegWit
374
+ - `141`: native SegWit root-branch vectors
375
+
376
+ ## Ethereum Experience
377
+
378
+ Ethereum accounts are created through [`SkeletonKey::Chains::Ethereum::Account`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/chains/ethereum/account.rb).
379
+
380
+ ```ruby
381
+ account = keyring.ethereum(purpose: 44, coin_type: 60, account_index: 0)
382
+
383
+ account.path
384
+ ```
385
+
386
+ Derive an address:
387
+
388
+ ```ruby
389
+ node = account.address(change: 0, index: 0)
390
+
391
+ node[:path]
392
+ node[:private_key] # hex, no 0x
393
+ node[:public_key] # 64-byte uncompressed payload, hex
394
+ node[:compressed_public_key]
395
+ node[:address] # EIP-55 checksummed 0x...
396
+ ```
397
+
398
+ Derive branch extended keys:
399
+
400
+ ```ruby
401
+ branch = account.branch_extended_keys(change: 0)
402
+
403
+ branch[:path]
404
+ branch[:xprv]
405
+ branch[:xpub]
406
+ ```
407
+
408
+ Supported Ethereum purposes:
409
+
410
+ - `32`: legacy BIP32 root mode
411
+ - `44`: BIP44 `m/44'/60'/account'/change/index`
412
+
413
+ ## Solana Experience
414
+
415
+ Solana accounts are created through [`SkeletonKey::Chains::Solana::Account`](/home/sebscholl/Code/skeleton-key/lib/skeleton_key/chains/solana/account.rb).
416
+
417
+ ```ruby
418
+ account = keyring.solana(account_index: 0)
419
+ account.path
420
+ ```
421
+
422
+ Derive a wallet-style Solana address:
423
+
424
+ ```ruby
425
+ node = account.address(change: 0)
426
+
427
+ node[:path]
428
+ node[:private_key] # 32-byte private seed, hex
429
+ node[:public_key] # 32-byte Ed25519 public key, hex
430
+ node[:address] # Base58-encoded Solana address
431
+ ```
432
+
433
+ Derive deeper hardened children:
434
+
435
+ ```ruby
436
+ node = account.address(change: 0, index: 15)
437
+ ```
438
+
439
+ Solana in SkeletonKey is hardened-only. There is no supported unhardened child derivation path.
440
+
441
+ ## Architecture
442
+
443
+ The architectural reference lives in [ARCHITECTURE.md](ARCHITECTURE.md). In short:
444
+
445
+ - recovery layer:
446
+ - BIP39
447
+ - SLIP-0039
448
+ - shared layer:
449
+ - seed normalization
450
+ - entropy
451
+ - BIP32
452
+ - SLIP-0010
453
+ - generic extended-key serialization
454
+ - Bitcoin layer:
455
+ - WIF
456
+ - Base58Check
457
+ - Bech32
458
+ - script/address semantics
459
+ - Ethereum layer:
460
+ - path conventions
461
+ - Keccak address derivation
462
+ - EIP-55 checksums
463
+ - Solana layer:
464
+ - hardened path conventions
465
+ - Ed25519 key generation
466
+ - Base58 address encoding
467
+
468
+ Rules that matter:
469
+
470
+ - Bitcoin address logic must not leak into shared derivation code.
471
+ - Ethereum must not inherit Bitcoin encodings or Bitcoin-style field naming.
472
+ - Solana must not inherit secp256k1 assumptions or Ethereum hashing rules.
473
+
474
+ ## Testing and Validation
475
+
476
+ Run the full suite:
477
+
478
+ ```bash
479
+ bundle exec rspec
480
+ ```
481
+
482
+ Run a focused file:
483
+
484
+ ```bash
485
+ bundle exec rspec spec/lib/skeleton_key/recovery/slip39_spec.rb
486
+ ```
487
+
488
+ Run all integration vectors:
489
+
490
+ ```bash
491
+ bundle exec rspec spec/integration/vectors
492
+ ```
493
+
494
+ Fixture layout:
495
+
496
+ - `spec/fixtures/recovery/`: BIP39 and SLIP-0039 recovery goldens
497
+ - `spec/fixtures/vectors/bitcoin/`: Bitcoin derivation vectors
498
+ - `spec/fixtures/vectors/ethereum/`: Ethereum derivation vectors
499
+ - `spec/fixtures/vectors/solana/`: Solana derivation vectors
500
+ - `spec/fixtures/codecs/`: Base58/Base58Check/Bech32 codec goldens
501
+
502
+ The preferred validation model in this repository is external golden-master comparison against established tools and independently generated corpora.
503
+
504
+ ## Fixture Policy
505
+
506
+ Golden-master fixtures in this repository are frozen validation artifacts, not routine developer outputs.
507
+
508
+ - do not casually regenerate fixtures as part of ordinary feature work
509
+ - treat fixture diffs as safety-critical review items
510
+ - when a fixture must change, document the external source and validation reason in the commit or PR
511
+ - prefer adding new coverage over rewriting existing canonical corpora
512
+
513
+ ## Repository Layout
514
+
515
+ Key directories:
516
+
517
+ - `lib/skeleton_key/recovery/`: BIP39 and SLIP-0039 recovery
518
+ - `lib/skeleton_key/derivation/`: BIP32, SLIP-0010, derivation paths
519
+ - `lib/skeleton_key/chains/bitcoin/`: Bitcoin account and support logic
520
+ - `lib/skeleton_key/chains/ethereum/`: Ethereum account and support logic
521
+ - `lib/skeleton_key/chains/solana/`: Solana account and support logic
522
+ - `lib/skeleton_key/codecs/`: local Base58, Base58Check, Bech32 codecs
523
+ - `spec/lib/`: unit specs
524
+ - `spec/integration/`: vector compliance specs
525
+ - `spec/support/`: shared spec helpers
526
+
527
+ ## Contributing
528
+
529
+ Read these first:
530
+
531
+ - [AGENTS.md](AGENTS.md)
532
+ - [ARCHITECTURE.md](ARCHITECTURE.md)
533
+ - [DOCUMENTATION_STYLE.md](DOCUMENTATION_STYLE.md)
534
+
535
+ Before submitting changes:
536
+
537
+ 1. keep the architecture boundary intact
538
+ 2. add or update golden fixtures when behavior changes
539
+ 3. add unit and integration coverage
540
+ 4. run `bundle exec rspec`
541
+
542
+ If you change recovery, derivation, encoding, key serialization, or address construction, external vector proof is required.
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "skeleton_key"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
data/bin/lint ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "rake"
6
+
7
+ APP_ROOT = File.expand_path("..", __dir__)
8
+ Dir.chdir(APP_ROOT)
9
+
10
+ system("bundle", "exec", "rake", "lint") || abort("Lint failed")
data/bin/setup ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+
6
+ # Ensure we're at the gem's root
7
+ APP_ROOT = File.expand_path("..", __dir__)
8
+ Dir.chdir(APP_ROOT)
9
+
10
+ # 1. Install dependencies
11
+ system("bundle install") || abort("❌ Bundle install failed")
12
+
13
+ # 2. Set up local development environment
14
+ FileUtils.mkdir_p("tmp")
15
+
16
+ # 3. Optionally run binstubs install
17
+ system("bundle binstubs bundler --force")
18
+
19
+ puts "✅ Setup complete! You can now run:"
20
+ puts " bin/console # start a console with SkeletonKey loaded"
21
+ puts " rake # run the default rake task (tests/lint)"
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ # Represents a Bitcoin HD account node derived via BIP32/BIP84.
7
+ #
8
+ # Handles derivation of xprv/xpub for a given account path.
9
+ #
10
+ # @example Default mainnet account
11
+ # acct = SkeletonKey::Chains::Bitcoin::Account.new(seed: seed.bytes)
12
+ # acct.xprv # => "xprv..."
13
+ # acct.xpub # => "xpub..."
14
+ class Account
15
+ include Support
16
+ include AccountDerivation
17
+ include Derivation::BIP32
18
+
19
+ attr_reader :purpose, :coin_type, :account_index, :network, :derived
20
+
21
+ LEGACY_BIP32_PURPOSE = 32
22
+ LEGACY_BIP141_PURPOSE = 141
23
+ DEFAULT_PURPOSE = 84
24
+ MAINNET_COIN = 0
25
+ TESTNET_COIN = 1
26
+
27
+ # @param seed [String] raw seed bytes
28
+ # @param purpose [Integer] derivation purpose (44, 49, 84)
29
+ # @param coin_type [Integer] 0 = mainnet, 1 = testnet
30
+ # @param account_index [Integer] account number (hardened)
31
+ # @param network [:mainnet, :testnet]
32
+ def initialize(seed:, purpose: DEFAULT_PURPOSE, coin_type: MAINNET_COIN, account_index: 0, network: :mainnet)
33
+ @purpose = purpose
34
+ @coin_type = coin_type
35
+ @account_index = account_index
36
+ @network = network
37
+
38
+ @derived = derive_from_seed(
39
+ seed,
40
+ purpose: purpose,
41
+ coin_type: coin_type,
42
+ account: account_index
43
+ )
44
+ end
45
+
46
+ # @return [String] BIP32 extended private key (xprv, zprv if re-encoded)
47
+ def xprv
48
+ @derived[:xprv]
49
+ end
50
+
51
+ # @return [String] BIP32 extended public key (xpub, zpub if re-encoded)
52
+ def xpub
53
+ @derived[:xpub]
54
+ end
55
+
56
+ # @return [String] BIP32 path
57
+ def path
58
+ return "m" if legacy_root_branch?
59
+
60
+ "m/#{purpose}'/#{coin_type}'/#{account_index}'"
61
+ end
62
+
63
+ # Derive a child address from this account.
64
+ #
65
+ # @param change [Integer] 0 = external, 1 = internal/change
66
+ # @param index [Integer] address index
67
+ # @return [Hash] derived address details (privkey, pubkey, wif, bech32, etc.)
68
+ def address(change: 0, index: 0, hardened_change: false, hardened_index: false)
69
+ derive_address_from_account(
70
+ change: change,
71
+ index: index,
72
+ hardened_change: hardened_change,
73
+ hardened_index: hardened_index
74
+ )
75
+ end
76
+
77
+ def branch_extended_keys(change: 0, hardened_change: false)
78
+ derive_branch_extended_keys(change: change, hardened_change: hardened_change)
79
+ end
80
+
81
+ private
82
+
83
+ def legacy_bip32?
84
+ purpose == LEGACY_BIP32_PURPOSE
85
+ end
86
+
87
+ def legacy_bip141?
88
+ purpose == LEGACY_BIP141_PURPOSE
89
+ end
90
+
91
+ def legacy_root_branch?
92
+ legacy_root_branch_purpose?(purpose)
93
+ end
94
+
95
+ def legacy_root_branch_purpose?(value)
96
+ [LEGACY_BIP32_PURPOSE, LEGACY_BIP141_PURPOSE].include?(value)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end