Skip to content

Commit

Permalink
Adding support for bitcoin testnet
Browse files Browse the repository at this point in the history
  • Loading branch information
wink committed Dec 24, 2013
1 parent 047e481 commit a049916
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 39 deletions.
7 changes: 4 additions & 3 deletions lib/money-tree/address.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module MoneyTree
class Address
attr_accessor :private_key, :public_key
attr_reader :private_key, :public_key

def initialize(opts = {})
@private_key = MoneyTree::PrivateKey.new key: opts[:private_key]
@public_key = MoneyTree::PublicKey.new(@private_key)
private_key = opts.delete(:private_key)
@private_key = MoneyTree::PrivateKey.new({ key: private_key }.merge(opts))
@public_key = MoneyTree::PublicKey.new(@private_key, opts)
end

def to_s
Expand Down
52 changes: 39 additions & 13 deletions lib/money-tree/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class KeyFormatNotFound < Exception; end
class InvalidWIFFormat < Exception; end
class InvalidBase64Format < Exception; end

attr_reader :options, :key, :raw_key
attr_reader :options, :key, :raw_key, :network, :network_key
attr_accessor :ec_key

GROUP_NAME = 'secp256k1'
Expand All @@ -40,6 +40,8 @@ class PrivateKey < Key
def initialize(opts = {})
@options = opts
@ec_key = PKey::EC.new GROUP_NAME
@network_key = options[:network] || :bitcoin
@network = MoneyTree::NETWORKS[network_key]
if @options[:key]
@raw_key = @options[:key]
@key = parse_raw_key
Expand Down Expand Up @@ -71,8 +73,8 @@ def set_public_key(opts = {})
end

def parse_raw_key
result = if raw_key.is_a?(Bignum) then int_to_hex(raw_key)
elsif hex_format? then raw_key
result = if raw_key.is_a?(Bignum) then from_bignum
elsif hex_format? then from_hex
elsif base64_format? then from_base64
elsif compressed_wif_format? then from_wif
elsif uncompressed_wif_format? then from_wif
Expand All @@ -81,28 +83,48 @@ def parse_raw_key
end
result.downcase
end

def from_bignum(bignum = raw_key)
int_to_hex(bignum)
end

def from_hex(hex = raw_key)
hex
end

def from_wif(wif = raw_key)
compressed = wif.length == 52
parse_network_from_wif(wif, compressed: compressed)
validate_wif(wif)
hex = decode_base58(wif)
last_char = compressed ? -11 : -9
hex.slice(2..last_char)
end

def parse_network_from_wif(wif, opts = {})
networks = MoneyTree::NETWORKS
chars_key = opts[:compressed] ? :compressed_wif_chars : :uncompressed_wif_chars
@network_key = networks.keys.select do |k|
networks[k][chars_key].include?(wif.slice(0))
end.first
@network = networks[network_key]
end

def from_base64(base64_key = raw_key)
raise InvalidBase64Format unless base64_format?(base64_key)
decode_base64(base64_key)
end

def compressed_wif_format?
raw_key.length == 52 && MoneyTree::NETWORKS[:bitcoin][:compressed_wif_chars].include?(raw_key.slice(0))
compressed_wif_chars = MoneyTree::NETWORKS.map {|k, v| v[:compressed_wif_chars]}.flatten
raw_key.length == 52 && compressed_wif_chars.include?(raw_key.slice(0))
end

def uncompressed_wif_format?
raw_key.length == 51 && raw_key.slice(0) == MoneyTree::NETWORKS[:bitcoin][:uncompressed_wif_char]
uncompressed_wif_chars = MoneyTree::NETWORKS.map {|k, v| v[:uncompressed_wif_chars]}.flatten
raw_key.length == 51 && uncompressed_wif_chars.include?(raw_key.slice(0))
end

def base64_format?(base64_key = raw_key)
base64_key.length == 44 && base64_key =~ /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
end
Expand All @@ -117,8 +139,8 @@ def to_hex

def to_wif(opts = {})
opts[:compressed] = true unless opts[:compressed] == false
source = MoneyTree::NETWORKS[:bitcoin][:privkey_version] + to_hex
source += MoneyTree::NETWORKS[:bitcoin][:privkey_compression_flag] if opts[:compressed]
source = network[:privkey_version] + to_hex
source += network[:privkey_compression_flag] if opts[:compressed]
hash = sha256(source)
hash = sha256(hash)
checksum = hash.slice(0..7)
Expand All @@ -128,7 +150,7 @@ def to_wif(opts = {})

def wif_valid?(wif)
hex = decode_base58(wif)
return false unless hex.slice(0..1) == MoneyTree::NETWORKS[:bitcoin][:privkey_version]
return false unless hex.slice(0..1) == network[:privkey_version]
checksum = hex.chars.to_a.pop(8).join
source = hex.slice(0..-9)
hash = sha256(source)
Expand Down Expand Up @@ -160,10 +182,14 @@ def initialize(p_key, opts = {})

if p_key.is_a?(PrivateKey)
@private_key = p_key
@network_key = private_key.network_key
@network = MoneyTree::NETWORKS[network_key]
@point = @private_key.calculate_public_key(@options)
@group = @point.group
@key = @raw_key = to_hex
else
@network_key = @options[:network] || :bitcoin
@network = MoneyTree::NETWORKS[network_key]
@raw_key = p_key
@group = PKey::EC::Group.new GROUP_NAME
@key = parse_raw_key
Expand All @@ -180,13 +206,13 @@ def compression=(compression_type = :compressed)
end

def compressed
compressed_key = self.class.new raw_key # deep clone
compressed_key = self.class.new raw_key, options # deep clone
compressed_key.set_point to_i, compressed: true
compressed_key
end

def uncompressed
uncompressed_key = self.class.new raw_key # deep clone
uncompressed_key = self.class.new raw_key, options # deep clone
uncompressed_key.set_point to_i, compressed: false
uncompressed_key
end
Expand Down Expand Up @@ -236,7 +262,7 @@ def to_ripemd160

def to_address
hash = to_ripemd160
address = MoneyTree::NETWORKS[:bitcoin][:address_version] + hash
address = network[:address_version] + hash
to_serialized_base58 address
end
alias :to_s :to_address
Expand Down
14 changes: 8 additions & 6 deletions lib/money-tree/networks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ module MoneyTree
bitcoin: {
address_version: '00',
p2sh_version: '05',
p2sh_char: '3',
privkey_version: '80',
privkey_compression_flag: '01',
extended_privkey_version: "0488ade4",
extended_pubkey_version: "0488b21e",
compressed_wif_chars: %w(K L),
uncompressed_wif_char: '5',
uncompressed_wif_chars: %w(5),
protocol_version: 70001
},
bitcoin_testnet: {
address_version: '6f',
p2sh_version: '05',
privkey_version: '80',
p2sh_version: 'c4',
p2sh_char: '2',
privkey_version: 'ef',
privkey_compression_flag: '01',
extended_privkey_version: "04358394",
extended_pubkey_version: "043587cf",
compressed_wif_chars: %w(K L),
uncompressed_wif_char: '5',
compressed_wif_chars: %w(c),
uncompressed_wif_chars: %w(9),
protocol_version: 70001
}
}
end
end
43 changes: 27 additions & 16 deletions lib/money-tree/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ module MoneyTree
class Node
include Support
extend Support
attr_reader :private_key, :public_key, :chain_code, :is_private, :depth, :index, :parent, :is_test
attr_reader :private_key, :public_key, :chain_code, :is_private, :depth, :index, :parent, :network, :network_key

class PublicDerivationFailure < Exception; end
class InvalidKeyForIndex < Exception; end
class ImportError < Exception; end
class PrivatePublicMismatch < Exception; end

def initialize(opts = {})
@network_key = opts.delete(:network) || :bitcoin
@network = MoneyTree::NETWORKS[network_key]
opts.each { |k, v| instance_variable_set "@#{k}", v }
end

def self.from_serialized_address(address)
hex = from_serialized_base58 address
version = from_version_hex hex.slice!(0..7)
self.new({
is_test: version[:test],
depth: hex.slice!(0..1).to_i(16),
fingerprint: hex.slice!(0..7),
index: hex.slice!(0..7).to_i(16),
Expand All @@ -26,11 +27,12 @@ def self.from_serialized_address(address)
end

def self.key_options(hex, version)
k_opts = { network: version[:network] }
if version[:private_key] && hex.slice(0..1) == '00'
private_key = MoneyTree::PrivateKey.new key: hex.slice(2..-1)
{ private_key: private_key, public_key: MoneyTree::PublicKey.new(private_key) }
private_key = MoneyTree::PrivateKey.new({ key: hex.slice(2..-1) }.merge(k_opts))
k_opts.merge private_key: private_key, public_key: MoneyTree::PublicKey.new(private_key)
elsif %w(02 03).include? hex.slice(0..1)
{ public_key: MoneyTree::PublicKey.new(hex) }
k_opts.merge public_key: MoneyTree::PublicKey.new(hex, k_opts)
else
raise ImportError, 'Public or private key data does not match version type'
end
Expand All @@ -39,13 +41,13 @@ def self.key_options(hex, version)
def self.from_version_hex(hex)
case hex
when MoneyTree::NETWORKS[:bitcoin][:extended_privkey_version]
{ private_key: true, test: false }
{ private_key: true, network: :bitcoin }
when MoneyTree::NETWORKS[:bitcoin][:extended_pubkey_version]
{ private_key: false, test: false }
{ private_key: false, network: :bitcoin }
when MoneyTree::NETWORKS[:bitcoin_testnet][:extended_privkey_version]
{ private_key: true, test: true }
{ private_key: true, network: :bitcoin_testnet }
when MoneyTree::NETWORKS[:bitcoin_testnet][:extended_pubkey_version]
{ private_key: false, test: true }
{ private_key: false, network: :bitcoin_testnet }
else
raise ImportError, 'invalid version bytes'
end
Expand Down Expand Up @@ -114,7 +116,7 @@ def right_from_hash(hash)
def to_serialized_hex(type = :public)
raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version
hex = MoneyTree::NETWORKS[:bitcoin][version_key] # version (4 bytes)
hex = network[version_key] # version (4 bytes)
hex += depth_hex(depth) # depth (1 byte)
hex += depth.zero? ? '00000000' : parent.to_fingerprint# fingerprint of key (4 bytes)
hex += index_hex(index) # child number i (4 bytes)
Expand All @@ -136,21 +138,22 @@ def to_fingerprint
end

def to_address
address = MoneyTree::NETWORKS[:bitcoin][:address_version] + to_identifier
address = network[:address_version] + to_identifier
to_serialized_base58 address
end

def subnode(i = 0, opts = {})
if private_key.nil?
child_public_key, child_chain_code = derive_public_key(i)
child_public_key = MoneyTree::PublicKey.new child_public_key
child_public_key = MoneyTree::PublicKey.new child_public_key, network: network_key
else
child_private_key, child_chain_code = derive_private_key(i)
child_private_key = MoneyTree::PrivateKey.new key: child_private_key
child_private_key = MoneyTree::PrivateKey.new key: child_private_key, network: network_key
child_public_key = MoneyTree::PublicKey.new child_private_key
end

MoneyTree::Node.new depth: depth+1,
MoneyTree::Node.new network: network_key,
depth: depth+1,
index: i,
private_key: private_key.nil? ? nil : child_private_key,
public_key: child_public_key,
Expand Down Expand Up @@ -233,6 +236,8 @@ def initialize(opts = {})
@depth = 0
@index = 0
opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex]
@network_key = opts[:network] || :bitcoin
@network = MoneyTree::NETWORKS[network_key]
if opts[:seed]
@seed = opts[:seed]
@seed_hash = generate_seed_hash(@seed)
Expand All @@ -243,9 +248,15 @@ def initialize(opts = {})
@chain_code = opts[:chain_code]
if opts[:private_key]
@private_key = opts[:private_key]
@network_key = @private_key.network_key
@network = MoneyTree::NETWORKS[network_key]
@public_key = MoneyTree::PublicKey.new @private_key
else opts[:public_key]
@public_key = opts[:public_key].is_a?(MoneyTree::PublicKey) ? opts[:public_key] : MoneyTree::PublicKey.new(opts[:public_key])
@public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey)
opts[:public_key]
else
MoneyTree::PublicKey.new(opts[:public_key], network: network_key)
end
end
else
generate_seed
Expand Down Expand Up @@ -274,7 +285,7 @@ def seed_valid?(seed_hash)
end

def set_seeded_keys
@private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash)
@private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash), network: network_key
@chain_code = right_from_hash(seed_hash)
@public_key = MoneyTree::PublicKey.new @private_key
end
Expand Down
10 changes: 10 additions & 0 deletions spec/lib/money-tree/address_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@
@address.private_key.to_s.should == "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
end
end

context "testnet3" do
before do
@address = MoneyTree::Address.new network: :bitcoin_testnet
end

it "returns a testnet address" do
%w(m n).should include(@address.to_s[0])
end
end
end
54 changes: 53 additions & 1 deletion spec/lib/money-tree/node_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,59 @@
@master.seed.bytesize.should == 32
end
end


context "testnet" do
before do
@master = MoneyTree::Master.new network: :bitcoin_testnet
end

it "generates testnet address" do
%w(m n).should include(@master.to_address[0])
end

it "generates testnet compressed wif" do
@master.private_key.to_wif[0].should == 'c'
end

it "generates testnet uncompressed wif" do
@master.private_key.to_wif(compressed: false)[0].should == '9'
end

it "generates testnet serialized private address" do
@master.to_serialized_address(:private).slice(0, 4).should == "tprv"
end

it "generates testnet serialized public address" do
@master.to_serialized_address.slice(0, 4).should == "tpub"
end

it "imports from testnet serialized private address" do
node = MoneyTree::Node.from_serialized_address 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE'
node.to_serialized_address(:private).should == 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE'
end

it "imports from testnet serialized public address" do
node = MoneyTree::Node.from_serialized_address 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT'
%w(m n).should include(node.public_key.to_s[0])
node.to_serialized_address.should == 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT'
end

it "generates testnet subnodes from serialized private address" do
node = MoneyTree::Node.from_serialized_address 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE'
subnode = node.node_for_path('1/1/1')
%w(m n).should include(subnode.public_key.to_s[0])
subnode.to_serialized_address(:private).slice(0,4).should == 'tprv'
subnode.to_serialized_address.slice(0,4).should == 'tpub'
end

it "generates testnet subnodes from serialized public address" do
node = MoneyTree::Node.from_serialized_address 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT'
subnode = node.node_for_path('1/1/1')
%w(m n).should include(subnode.public_key.to_s[0])
subnode.to_serialized_address.slice(0,4).should == 'tpub'
end
end

describe "Test vector 1" do
describe "from a seed" do
before do
Expand Down
Loading

0 comments on commit a049916

Please sign in to comment.