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
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ # Internal account-derivation helpers for Bitcoin account objects.
7
+ module AccountDerivation
8
+ private
9
+
10
+ def derive_from_seed(seed_bytes, purpose: 84, coin_type: 0, account: 0)
11
+ return derive_legacy_root_from_seed(seed_bytes) if legacy_root_branch_purpose?(purpose)
12
+
13
+ derive_standard_account_from_seed(seed_bytes, purpose: purpose, coin_type: coin_type, account: account)
14
+ end
15
+
16
+ def derive_standard_account_from_seed(seed_bytes, purpose:, coin_type:, account:)
17
+ key, chain_code = master_from_seed(seed_bytes)
18
+ depth = 0
19
+
20
+ key, chain_code, depth, = derive_step(key, chain_code, depth, hardened(purpose))
21
+ key, chain_code, depth, = derive_step(key, chain_code, depth, hardened(coin_type))
22
+ account_key, account_chain_code, account_depth, account_parent_fpr, account_child_num =
23
+ derive_step(key, chain_code, depth, hardened(account))
24
+
25
+ serialize_standard_account(
26
+ account_key,
27
+ account_chain_code,
28
+ account_depth,
29
+ account_parent_fpr,
30
+ account_child_num
31
+ )
32
+ end
33
+
34
+ def derive_legacy_root_from_seed(seed_bytes)
35
+ master_key, master_chain_code = master_from_seed(seed_bytes)
36
+ master_pubkey = privkey_to_pubkey_compressed(master_key)
37
+ master_fingerprint = fingerprint_from_pubkey(master_pubkey)
38
+ branch_key, branch_chain_code = ckd_priv(master_key, master_chain_code, 0)
39
+ branch_pubkey = privkey_to_pubkey_compressed(branch_key)
40
+
41
+ {
42
+ k_int: master_key,
43
+ c: master_chain_code,
44
+ bip32_xprv: serialize_xprv(
45
+ branch_key,
46
+ branch_chain_code,
47
+ depth: 1,
48
+ parent_fpr: master_fingerprint,
49
+ child_num: 0,
50
+ version: version_byte(network: network, purpose: purpose, private: true)
51
+ ),
52
+ bip32_xpub: serialize_xpub(
53
+ branch_pubkey,
54
+ branch_chain_code,
55
+ depth: 1,
56
+ parent_fpr: master_fingerprint,
57
+ child_num: 0,
58
+ version: version_byte(network: network, purpose: purpose)
59
+ ),
60
+ account_xprv: "",
61
+ account_xpub: ""
62
+ }
63
+ end
64
+
65
+ def derive_step(parent_key, parent_chain_code, parent_depth, child_index)
66
+ parent_pubkey = privkey_to_pubkey_compressed(parent_key)
67
+ parent_fingerprint = fingerprint_from_pubkey(parent_pubkey)
68
+ child_key, child_chain_code = ckd_priv(parent_key, parent_chain_code, child_index)
69
+
70
+ [child_key, child_chain_code, parent_depth + 1, parent_fingerprint, child_index]
71
+ end
72
+
73
+ def serialize_standard_account(account_key, account_chain_code, account_depth, account_parent_fpr, account_child_num)
74
+ account_pubkey = privkey_to_pubkey_compressed(account_key)
75
+ branch_key, branch_chain_code, branch_depth, branch_parent_fpr, branch_child_num = derive_step(
76
+ account_key,
77
+ account_chain_code,
78
+ account_depth,
79
+ 0
80
+ )
81
+ branch_pubkey = privkey_to_pubkey_compressed(branch_key)
82
+
83
+ {
84
+ k_int: account_key,
85
+ c: account_chain_code,
86
+ bip32_xprv: serialize_xprv(
87
+ branch_key,
88
+ branch_chain_code,
89
+ depth: branch_depth,
90
+ parent_fpr: branch_parent_fpr,
91
+ child_num: branch_child_num,
92
+ version: version_byte(network: network, purpose: purpose, private: true)
93
+ ),
94
+ bip32_xpub: serialize_xpub(
95
+ branch_pubkey,
96
+ branch_chain_code,
97
+ depth: branch_depth,
98
+ parent_fpr: branch_parent_fpr,
99
+ child_num: branch_child_num,
100
+ version: version_byte(network: network, purpose: purpose)
101
+ ),
102
+ account_xprv: serialize_xprv(
103
+ account_key,
104
+ account_chain_code,
105
+ depth: account_depth,
106
+ parent_fpr: account_parent_fpr,
107
+ child_num: account_child_num,
108
+ version: version_byte(network: network, purpose: purpose, private: true)
109
+ ),
110
+ account_xpub: serialize_xpub(
111
+ account_pubkey,
112
+ account_chain_code,
113
+ depth: account_depth,
114
+ parent_fpr: account_parent_fpr,
115
+ child_num: account_child_num,
116
+ version: version_byte(network: network, purpose: purpose)
117
+ )
118
+ }
119
+ end
120
+
121
+ def hardened(index)
122
+ index | 0x8000_0000
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ module Support
7
+ module Outputs
8
+ module_function
9
+
10
+ def derive_address_from_account(change: 0, index: 0, hardened_change: false, hardened_index: false)
11
+ k, c = derived[:k_int], derived[:c]
12
+ change_index = hardened_change ? change | Derivation::Path::HARDENED_FLAG : change
13
+ address_index = hardened_index ? index | Derivation::Path::HARDENED_FLAG : index
14
+
15
+ k, c = ckd_priv(k, c, change_index)
16
+ k, c = ckd_priv(k, c, address_index)
17
+
18
+ pub = privkey_to_pubkey_compressed(k)
19
+
20
+ {
21
+ path: build_derived_path(change: change, index: index, hardened_change: hardened_change, hardened_index: hardened_index),
22
+ privkey: k,
23
+ pubkey: pub,
24
+ chain_code: c,
25
+ wif: to_wif(ser256(k), network: network),
26
+ address: address_for_pubkey(pub)
27
+ }
28
+ end
29
+
30
+ def derive_branch_extended_keys(change: 0, hardened_change: false)
31
+ parent_key = derived[:k_int]
32
+ parent_chain_code = derived[:c]
33
+ parent_pubkey = privkey_to_pubkey_compressed(parent_key)
34
+ child_num = hardened_change ? change | Derivation::Path::HARDENED_FLAG : change
35
+ branch_key, branch_chain_code = ckd_priv(parent_key, parent_chain_code, child_num)
36
+ branch_pubkey = privkey_to_pubkey_compressed(branch_key)
37
+
38
+ {
39
+ path: branch_derived_path(change: change, hardened_change: hardened_change),
40
+ xprv: serialize_xprv(
41
+ branch_key,
42
+ branch_chain_code,
43
+ depth: legacy_root_branch? ? 1 : 4,
44
+ parent_fpr: fingerprint_from_pubkey(parent_pubkey),
45
+ child_num: child_num,
46
+ version: version_byte(network: network, purpose: purpose, private: true)
47
+ ),
48
+ xpub: serialize_xpub(
49
+ branch_pubkey,
50
+ branch_chain_code,
51
+ depth: legacy_root_branch? ? 1 : 4,
52
+ parent_fpr: fingerprint_from_pubkey(parent_pubkey),
53
+ child_num: child_num,
54
+ version: version_byte(network: network, purpose: purpose)
55
+ )
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def address_for_pubkey(pubkey)
62
+ case purpose
63
+ when 32, 44
64
+ to_p2pkh_address(pubkey, network: network)
65
+ when 49
66
+ to_p2sh_p2wpkh_address(pubkey, network: network)
67
+ when 84, 141
68
+ to_bech32_address(pubkey, hrp: network == :mainnet ? "bc" : "tb")
69
+ else
70
+ raise Errors::UnsupportedPurposeError.new(purpose)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ module Support
7
+ module Paths
8
+ module_function
9
+
10
+ def build_derived_path(change:, index:, hardened_change:, hardened_index:)
11
+ rendered_change = hardened_change ? "#{change}'" : change.to_s
12
+ rendered_index = hardened_index ? "#{index}'" : index.to_s
13
+
14
+ if legacy_root_branch?
15
+ "m/#{rendered_change}/#{rendered_index}"
16
+ else
17
+ "m/#{purpose}'/#{coin_type}'/#{account_index}'/#{rendered_change}/#{rendered_index}"
18
+ end
19
+ end
20
+
21
+ def branch_derived_path(change:, hardened_change:)
22
+ rendered_change = hardened_change ? "#{change}'" : change.to_s
23
+
24
+ if legacy_root_branch?
25
+ "m/#{rendered_change}"
26
+ else
27
+ "m/#{purpose}'/#{coin_type}'/#{account_index}'/#{rendered_change}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ module Support
7
+ module Versioning
8
+ VERSION_BYTES = {
9
+ mainnet: {
10
+ bip44: { xpub: 0x0488B21E, xprv: 0x0488ADE4 },
11
+ bip49: { ypub: 0x049D7CB2, yprv: 0x049D7878 },
12
+ bip84: { zpub: 0x04B24746, zprv: 0x04B2430C }
13
+ },
14
+ testnet: {
15
+ bip44: { tpub: 0x043587CF, tprv: 0x04358394 },
16
+ bip49: { upub: 0x044A5262, uprv: 0x044A4E28 },
17
+ bip84: { vpub: 0x045F1CF6, vprv: 0x045F18BC }
18
+ }
19
+ }.freeze
20
+
21
+ module_function
22
+
23
+ def version_byte(network:, purpose:, private: false)
24
+ version = if private
25
+ private_version_byte(network: network, purpose: purpose)
26
+ else
27
+ public_version_byte(network: network, purpose: purpose)
28
+ end
29
+
30
+ [version].pack("N")
31
+ end
32
+
33
+ def version_bytes(network:, purpose:)
34
+ case [purpose, network]
35
+ when [32, :mainnet] then VERSION_BYTES[:mainnet][:bip44]
36
+ when [32, :testnet] then VERSION_BYTES[:testnet][:bip44]
37
+ when [141, :mainnet] then VERSION_BYTES[:mainnet][:bip84]
38
+ when [141, :testnet] then VERSION_BYTES[:testnet][:bip84]
39
+ when [44, :mainnet] then VERSION_BYTES[:mainnet][:bip44]
40
+ when [44, :testnet] then VERSION_BYTES[:testnet][:bip44]
41
+ when [49, :mainnet] then VERSION_BYTES[:mainnet][:bip49]
42
+ when [49, :testnet] then VERSION_BYTES[:testnet][:bip49]
43
+ when [84, :mainnet] then VERSION_BYTES[:mainnet][:bip84]
44
+ when [84, :testnet] then VERSION_BYTES[:testnet][:bip84]
45
+ else
46
+ raise Errors::UnsupportedPurposeNetworkError.new(purpose: purpose, network: network)
47
+ end
48
+ end
49
+
50
+ def public_version_byte(network:, purpose:)
51
+ case [purpose, network]
52
+ when [32, :mainnet] then VERSION_BYTES[:mainnet][:bip44][:xpub]
53
+ when [32, :testnet] then VERSION_BYTES[:testnet][:bip44][:tpub]
54
+ when [141, :mainnet] then VERSION_BYTES[:mainnet][:bip84][:zpub]
55
+ when [141, :testnet] then VERSION_BYTES[:testnet][:bip84][:vpub]
56
+ when [44, :mainnet] then VERSION_BYTES[:mainnet][:bip44][:xpub]
57
+ when [44, :testnet] then VERSION_BYTES[:testnet][:bip44][:tpub]
58
+ when [49, :mainnet] then VERSION_BYTES[:mainnet][:bip49][:ypub]
59
+ when [49, :testnet] then VERSION_BYTES[:testnet][:bip49][:upub]
60
+ when [84, :mainnet] then VERSION_BYTES[:mainnet][:bip84][:zpub]
61
+ when [84, :testnet] then VERSION_BYTES[:testnet][:bip84][:vpub]
62
+ else
63
+ raise Errors::UnsupportedPurposeNetworkError.new(purpose: purpose, network: network)
64
+ end
65
+ end
66
+
67
+ def private_version_byte(network:, purpose:)
68
+ case [purpose, network]
69
+ when [32, :mainnet] then VERSION_BYTES[:mainnet][:bip44][:xprv]
70
+ when [32, :testnet] then VERSION_BYTES[:testnet][:bip44][:tprv]
71
+ when [141, :mainnet] then VERSION_BYTES[:mainnet][:bip84][:zprv]
72
+ when [141, :testnet] then VERSION_BYTES[:testnet][:bip84][:vprv]
73
+ when [44, :mainnet] then VERSION_BYTES[:mainnet][:bip44][:xprv]
74
+ when [44, :testnet] then VERSION_BYTES[:testnet][:bip44][:tprv]
75
+ when [49, :mainnet] then VERSION_BYTES[:mainnet][:bip49][:yprv]
76
+ when [49, :testnet] then VERSION_BYTES[:testnet][:bip49][:uprv]
77
+ when [84, :mainnet] then VERSION_BYTES[:mainnet][:bip84][:zprv]
78
+ when [84, :testnet] then VERSION_BYTES[:testnet][:bip84][:vprv]
79
+ else
80
+ raise Errors::UnsupportedPurposeNetworkError.new(purpose: purpose, network: network)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Bitcoin
6
+ module Support
7
+ extend Utils::Hashing
8
+ extend Utils::Encoding
9
+ include Versioning
10
+ include Paths
11
+ include Outputs
12
+
13
+ module_function
14
+
15
+ def to_wif(priv_bytes, compressed: true, network: :mainnet)
16
+ prefix = (network == :mainnet ? "\x80".b : "\xEF".b)
17
+ payload = prefix + priv_bytes.b
18
+ payload += "\x01".b if compressed
19
+ base58check_encode(payload)
20
+ end
21
+
22
+ def to_p2pkh_address(pubkey_bytes, network: :mainnet)
23
+ prefix = (network == :mainnet ? "\x00" : "\x6F")
24
+ payload = prefix + hash160(pubkey_bytes)
25
+ base58check_encode(payload)
26
+ end
27
+
28
+ def to_p2sh_p2wpkh_address(pubkey_bytes, network: :mainnet)
29
+ prog = hash160(pubkey_bytes)
30
+ redeem_script = "\x00\x14" + prog
31
+ script_hash = hash160(redeem_script)
32
+
33
+ prefix = (network == :mainnet ? "\x05" : "\xC4")
34
+ payload = prefix + script_hash
35
+ base58check_encode(payload)
36
+ end
37
+
38
+ def to_bech32_address(pubkey_bytes, hrp: "bc")
39
+ prog = hash160(pubkey_bytes)
40
+ prog5 = Codecs::Bech32.convert_bits(prog.bytes, 8, 5, true)
41
+ data = [0] + prog5
42
+
43
+ Codecs::Bech32.encode(hrp, data, Codecs::Bech32::Encoding::BECH32)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkeletonKey
4
+ module Chains
5
+ module Ethereum
6
+ ##
7
+ # Ethereum account derivation from a shared {Seed}.
8
+ #
9
+ # This class owns Ethereum path conventions and Ethereum-facing account
10
+ # semantics. Shared secp256k1 and BIP32 primitives remain in the derivation
11
+ # layer; this class decides which path to walk and how to expose the
12
+ # resulting branch and address data.
13
+ #
14
+ # Supported modes:
15
+ # - legacy BIP32 root mode (`purpose == 32`)
16
+ # - BIP44 account mode (`m/44'/60'/account'`)
17
+ #
18
+ # @example Derive the first Ethereum address from a BIP44 account
19
+ # account = SkeletonKey::Chains::Ethereum::Account.new(seed: seed.bytes)
20
+ # account.address(change: 0, index: 0)
21
+ class Account
22
+ include Support
23
+ include Derivation::BIP32
24
+
25
+ # @return [Integer] derivation purpose (32 or 44)
26
+ # @return [Integer] SLIP-0044 coin type, normally 60 for Ethereum
27
+ # @return [Integer] account index within the purpose/coin namespace
28
+ # @return [Hash] cached derived account metadata and extended keys
29
+ attr_reader :purpose, :coin_type, :account_index, :derived
30
+
31
+ LEGACY_BIP32_PURPOSE = 32
32
+ DEFAULT_PURPOSE = 44
33
+ ETHEREUM_COIN = 60
34
+
35
+ # @param seed [String] canonical seed bytes
36
+ # @param purpose [Integer] derivation purpose (32 or 44)
37
+ # @param coin_type [Integer] SLIP-0044 coin type
38
+ # @param account_index [Integer] account number to derive
39
+ # @raise [Errors::UnsupportedPurposeError] if the requested path family is unsupported
40
+ def initialize(seed:, purpose: DEFAULT_PURPOSE, coin_type: ETHEREUM_COIN, account_index: 0)
41
+ @purpose = purpose
42
+ @coin_type = coin_type
43
+ @account_index = account_index
44
+ @derived = derive_from_seed(seed, purpose: purpose, coin_type: coin_type, account: account_index)
45
+ end
46
+
47
+ # Returns the account path prefix that future branch and address
48
+ # derivations are anchored to.
49
+ #
50
+ # @return [String]
51
+ def path
52
+ derived[:path_prefix]
53
+ end
54
+
55
+ # Derives an Ethereum address node below the account prefix.
56
+ #
57
+ # Ethereum keeps the BIP44 `change/index` shape for compatibility even
58
+ # though the chain is account-based rather than UTXO-based.
59
+ #
60
+ # @param change [Integer] branch index beneath the account
61
+ # @param index [Integer] address index within the branch
62
+ # @param hardened_change [Boolean] whether to harden the branch step
63
+ # @param hardened_index [Boolean] whether to harden the address step
64
+ # @return [Hash] private key, public keys, checksummed address, and path metadata
65
+ def address(change: 0, index: 0, hardened_change: false, hardened_index: false)
66
+ derive_address_from_node(
67
+ change: change,
68
+ index: index,
69
+ hardened_change: hardened_change,
70
+ hardened_index: hardened_index
71
+ )
72
+ end
73
+
74
+ # Serializes the extended keypair for a branch directly beneath the
75
+ # current account or root node.
76
+ #
77
+ # @param change [Integer] branch index to derive
78
+ # @param hardened_change [Boolean] whether the branch child is hardened
79
+ # @return [Hash] serialized extended keypair and rendered path
80
+ def branch_extended_keys(change: 0, hardened_change: false)
81
+ derive_branch_extended_keys(change: change, hardened_change: hardened_change)
82
+ end
83
+
84
+ private
85
+
86
+ def derive_from_seed(seed_bytes, purpose:, coin_type:, account:)
87
+ return derive_legacy_root_from_seed(seed_bytes) if purpose == LEGACY_BIP32_PURPOSE
88
+ return derive_bip44_account_from_seed(seed_bytes, purpose: purpose, coin_type: coin_type, account: account) if purpose == 44
89
+
90
+ raise Errors::UnsupportedPurposeError.new(purpose)
91
+ end
92
+
93
+ def derive_legacy_root_from_seed(seed_bytes)
94
+ k_master, c_master = master_from_seed(seed_bytes)
95
+ master_pub = privkey_to_pubkey_compressed(k_master)
96
+ master_fpr = fingerprint_from_pubkey(master_pub)
97
+ k_branch, c_branch = ckd_priv(k_master, c_master, 0)
98
+ pub_branch = privkey_to_pubkey_compressed(k_branch)
99
+
100
+ {
101
+ path_prefix: "m",
102
+ k_int: k_master,
103
+ c: c_master,
104
+ account_extended_private_key: "",
105
+ account_extended_public_key: "",
106
+ branch_extended_private_key: serialize_xprv(
107
+ k_branch,
108
+ c_branch,
109
+ depth: 1,
110
+ parent_fpr: master_fpr,
111
+ child_num: 0,
112
+ version: extended_private_version
113
+ ),
114
+ branch_extended_public_key: serialize_xpub(
115
+ pub_branch,
116
+ c_branch,
117
+ depth: 1,
118
+ parent_fpr: master_fpr,
119
+ child_num: 0,
120
+ version: extended_public_version
121
+ )
122
+ }
123
+ end
124
+
125
+ def derive_bip44_account_from_seed(seed_bytes, purpose:, coin_type:, account:)
126
+ k_int, chain_code = master_from_seed(seed_bytes)
127
+ depth = 0
128
+
129
+ # Walk the BIP44 hardened account path one level at a time so the
130
+ # serialized parent fingerprint and child number remain explicit.
131
+ derive_step = ->(k_in, c_in, depth_in, index) do
132
+ parent_pub = privkey_to_pubkey_compressed(k_in)
133
+ parent_fpr = fingerprint_from_pubkey(parent_pub)
134
+ k_out, c_out = ckd_priv(k_in, c_in, index)
135
+ [k_out, c_out, depth_in + 1, parent_fpr, index]
136
+ end
137
+
138
+ k_int, chain_code, depth, _, _ = derive_step.call(k_int, chain_code, depth, 0x8000_0000 | purpose)
139
+ k_int, chain_code, depth, _, _ = derive_step.call(k_int, chain_code, depth, 0x8000_0000 | coin_type)
140
+ k_account, c_account, depth_account, parent_fpr, child_num = derive_step.call(k_int, chain_code, depth, 0x8000_0000 | account)
141
+ pub_account = privkey_to_pubkey_compressed(k_account)
142
+
143
+ k_branch, c_branch, depth_branch, branch_parent_fpr, branch_child_num = derive_step.call(k_account, c_account, depth_account, 0)
144
+ pub_branch = privkey_to_pubkey_compressed(k_branch)
145
+
146
+ {
147
+ path_prefix: "m/#{purpose}'/#{coin_type}'/#{account}'",
148
+ k_int: k_account,
149
+ c: c_account,
150
+ account_extended_private_key: serialize_xprv(
151
+ k_account,
152
+ c_account,
153
+ depth: depth_account,
154
+ parent_fpr: parent_fpr,
155
+ child_num: child_num,
156
+ version: extended_private_version
157
+ ),
158
+ account_extended_public_key: serialize_xpub(
159
+ pub_account,
160
+ c_account,
161
+ depth: depth_account,
162
+ parent_fpr: parent_fpr,
163
+ child_num: child_num,
164
+ version: extended_public_version
165
+ ),
166
+ branch_extended_private_key: serialize_xprv(
167
+ k_branch,
168
+ c_branch,
169
+ depth: depth_branch,
170
+ parent_fpr: branch_parent_fpr,
171
+ child_num: branch_child_num,
172
+ version: extended_private_version
173
+ ),
174
+ branch_extended_public_key: serialize_xpub(
175
+ pub_branch,
176
+ c_branch,
177
+ depth: depth_branch,
178
+ parent_fpr: branch_parent_fpr,
179
+ child_num: branch_child_num,
180
+ version: extended_public_version
181
+ )
182
+ }
183
+ end
184
+
185
+ def legacy_root_branch?
186
+ purpose == LEGACY_BIP32_PURPOSE
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end