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.
- checksums.yaml +7 -0
- data/README.md +542 -0
- data/bin/console +8 -0
- data/bin/lint +10 -0
- data/bin/setup +21 -0
- data/lib/skeleton_key/chains/bitcoin/account.rb +101 -0
- data/lib/skeleton_key/chains/bitcoin/account_derivation.rb +127 -0
- data/lib/skeleton_key/chains/bitcoin/support/outputs.rb +77 -0
- data/lib/skeleton_key/chains/bitcoin/support/paths.rb +34 -0
- data/lib/skeleton_key/chains/bitcoin/support/versioning.rb +87 -0
- data/lib/skeleton_key/chains/bitcoin/support.rb +48 -0
- data/lib/skeleton_key/chains/ethereum/account.rb +191 -0
- data/lib/skeleton_key/chains/ethereum/support.rb +143 -0
- data/lib/skeleton_key/chains/solana/account.rb +117 -0
- data/lib/skeleton_key/chains/solana/support.rb +27 -0
- data/lib/skeleton_key/codecs/base58.rb +64 -0
- data/lib/skeleton_key/codecs/base58_check.rb +42 -0
- data/lib/skeleton_key/codecs/bech32.rb +182 -0
- data/lib/skeleton_key/constants.rb +68 -0
- data/lib/skeleton_key/core/entropy.rb +37 -0
- data/lib/skeleton_key/derivation/bip32.rb +182 -0
- data/lib/skeleton_key/derivation/path.rb +112 -0
- data/lib/skeleton_key/derivation/slip10.rb +89 -0
- data/lib/skeleton_key/errors.rb +158 -0
- data/lib/skeleton_key/keyring.rb +63 -0
- data/lib/skeleton_key/recovery/bip39.rb +212 -0
- data/lib/skeleton_key/recovery/bip39_english.txt +2048 -0
- data/lib/skeleton_key/recovery/slip39.rb +220 -0
- data/lib/skeleton_key/recovery/slip39_support/bit_packing.rb +37 -0
- data/lib/skeleton_key/recovery/slip39_support/checksum.rb +53 -0
- data/lib/skeleton_key/recovery/slip39_support/cipher.rb +81 -0
- data/lib/skeleton_key/recovery/slip39_support/decoder.rb +109 -0
- data/lib/skeleton_key/recovery/slip39_support/encoder.rb +48 -0
- data/lib/skeleton_key/recovery/slip39_support/generated_set.rb +39 -0
- data/lib/skeleton_key/recovery/slip39_support/generator.rb +156 -0
- data/lib/skeleton_key/recovery/slip39_support/interpolation.rb +71 -0
- data/lib/skeleton_key/recovery/slip39_support/protocol.rb +34 -0
- data/lib/skeleton_key/recovery/slip39_support/secret_recovery.rb +74 -0
- data/lib/skeleton_key/recovery/slip39_support/share.rb +50 -0
- data/lib/skeleton_key/recovery/slip39_wordlist.txt +1024 -0
- data/lib/skeleton_key/seed.rb +127 -0
- data/lib/skeleton_key/skeleton_key.code-workspace +11 -0
- data/lib/skeleton_key/utils/encoding.rb +134 -0
- data/lib/skeleton_key/utils/hashing.rb +238 -0
- data/lib/skeleton_key/version.rb +8 -0
- data/lib/skeleton_key.rb +66 -0
- 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
|