Commit 9cfffcdb6f6212ec35b6260a44619b5444c3f105

Authored by Marius Hanne
1 parent 2f19a72180

wallet: create multisig outputs

Showing 2 changed files with 76 additions and 25 deletions Side-by-side Diff

lib/bitcoin/wallet/wallet.rb
... ... @@ -38,18 +38,31 @@
38 38 end
39 39  
40 40 # outputs = [<addr>, <value>]
  41 + # [:address, <addr>, <value>]
  42 + # [:multisig, 2, 3, <addr>, <addr>, <addr>, <value>]
41 43 def tx outputs, fee = 0, change_policy = :back
42   - prev_outs = get_selector.select(outputs.map{|o| o[1]}.inject(:+))
  44 + output_value = outputs.map{|o|o[-1]}.inject(:+)
  45 +
  46 + prev_outs = get_selector.select(output_value)
43 47 return nil if !prev_outs
44 48  
45 49 tx = Bitcoin::Protocol::Tx.new(nil)
46 50  
47 51 input_value = prev_outs.map(&:value).inject(:+)
48   - output_value = outputs.map{|o|o[1]}.inject(:+)
49 52 return nil unless input_value >= (output_value + fee)
50 53  
51   - outputs.each do |addr, value|
52   - script = Bitcoin::Script.to_address_script(addr)
  54 + outputs.each do |type, *addrs, value|
  55 + script = nil
  56 + case type
  57 + when :address
  58 + script = Bitcoin::Script.to_address_script(addrs[0])
  59 + when :multisig
  60 + m, *pubkeys = addrs
  61 + addrs = pubkeys.map{|p| Bitcoin.pubkey_to_address(p)}
  62 + script = Bitcoin::Script.to_multisig_script(m, *pubkeys)
  63 + else
  64 + raise "unknown script type: #{type}"
  65 + end
53 66 txout = Bitcoin::Protocol::TxOut.new(value, script.bytesize, script)
54 67 tx.add_out(txout)
55 68 end
56 69  
... ... @@ -70,10 +83,11 @@
70 83  
71 84 prev_outs.each_with_index do |prev_out, idx|
72 85 prev_tx = prev_out.get_tx
  86 +
73 87 key = @keystore.key(prev_out.get_address)
74 88 sig_hash = tx.signature_hash_for_input(idx, prev_tx)
75 89 sig = key.sign(sig_hash)
76   - script_sig = Bitcoin::Script.to_signature_pubkey_script(sig, [key.pub].pack("H*"))
  90 + script_sig = Bitcoin::Script.to_pubkey_script_sig(sig, [key.pub].pack("H*"))
77 91 tx.in[idx].script_sig_length = script_sig.bytesize
78 92 tx.in[idx].script_sig = script_sig
79 93 raise "Signature error" unless tx.verify_input_signature(idx, prev_tx)
spec/bitcoin/wallet/wallet_spec.rb
... ... @@ -16,27 +16,34 @@
16 16 @storage = Mock.new
17 17 @keystore = Mock.new
18 18 @key = Key.from_base58('5J2hn1E8KEXmQn5gqykzKotyCcHbKrVbe8fjegsfVXRdv6wwon8')
  19 + @addr = '1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'
  20 + @key2 = Key.from_base58('5KK9Lw8gtNd4kcaXQJmkwcmNy8Y5rLGm49RqhcYAb7qRhWxaWMJ')
  21 + @addr2 = '134A4Bi8jN5V2KjkwmXUHjokDqdyqZ778J'
19 22 @keystore.expect(:keys, [@key])
20 23 @selector = MiniTest::Mock.new
21 24 @wallet = Wallet.new(@storage, @keystore, @selector)
22 25 end
23 26  
  27 + after do
  28 + [@storage.verify, @selector.verify, @keystore.verify].all?.should == true
  29 + end
  30 +
24 31 it "should get total balance" do
25   - @storage.expect(:get_txouts_for_address, [], ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'])
  32 + @storage.expect(:get_txouts_for_address, [], [@addr])
26 33 @wallet.get_balance.should == 0
27 34  
28 35 @storage.expect(:get_txouts_for_address, [txout_mock(5000, nil)],
29   - ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'])
  36 + [@addr])
30 37 @wallet.get_balance.should == 5000
31 38  
32 39 @storage.expect(:get_txouts_for_address, [txout_mock(5000, true), txout_mock(1000, nil)],
33   - ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'])
  40 + [@addr])
34 41 @wallet.get_balance.should == 1000
35 42 @storage.verify; @keystore.verify
36 43 end
37 44  
38 45 it "should get all addrs" do
39   - @wallet.addrs.should == ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo']
  46 + @wallet.addrs.should == [@addr]
40 47  
41 48 @keystore.expect(:keys, [@key, Key.generate])
42 49 @wallet.addrs.size.should == 2
43 50  
... ... @@ -45,10 +52,10 @@
45 52  
46 53 it "should list all addrs with balances" do
47 54 @storage.expect(:get_balance, 0, ['dcbc93494b38ae96b14b1cc080d2acb514b7e955'])
48   - @wallet.list.should == [['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo', 0]]
  55 + @wallet.list.should == [[@addr, 0]]
49 56  
50 57 @storage.expect(:get_balance, 5000, ['dcbc93494b38ae96b14b1cc080d2acb514b7e955'])
51   - @wallet.list.should == [['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo', 5000]]
  58 + @wallet.list.should == [[@addr, 5000]]
52 59 @storage.verify
53 60 end
54 61  
... ... @@ -64,7 +71,6 @@
64 71 end
65 72  
66 73 describe "Bitcoin::Wallet::Wallet#tx" do
67   -
68 74 def txout_mock(value, next_in)
69 75 txout = Mock.new
70 76 txout.expect(:value, value)
71 77  
72 78  
73 79  
... ... @@ -77,18 +83,19 @@
77 83 tx.expect(:binary_hash, "foo")
78 84 tx.expect(:out, [txout])
79 85 txout.expect(:get_tx, tx)
80   - txout.expect(:get_address, "1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo")
  86 + txout.expect(:get_address, @addr)
81 87 txout.expect(:pk_script,
82   - Script.to_address_script('1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'))
83   - @storage.expect(:get_txouts_for_address, [txout],
84   - ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'])
85   - @keystore.expect(:key, @key, ['1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'])
  88 + Script.to_address_script(@addr))
  89 + @storage.expect(:get_txouts_for_address, [txout], [@addr])
  90 + @keystore.expect(:key, @key, [@addr])
86 91 selector = Mock.new
87 92 selector.expect(:select, [txout], [[txout]])
88 93 @selector.expect(:new, selector, [[txout]])
89   - @tx = @wallet.tx([['1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]])
  94 + @tx = @wallet.tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]])
90 95 end
91 96  
  97 +
  98 +
92 99 it "should have hash" do
93 100 @tx.hash.size.should == 64
94 101 end
95 102  
96 103  
... ... @@ -109,16 +116,16 @@
109 116 it "should have change output" do
110 117 @tx.out.last.value.should == 4000
111 118 s = Script.new(@tx.out.last.pk_script)
112   - s.get_address.should == '1M89ZeWtmZmATzE3b6PHTBi8c7tGsg5xpo'
  119 + s.get_address.should == @addr
113 120 end
114 121  
115 122 it "should leave tx fee" do
116   - @tx = @wallet.tx([['1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50)
  123 + @tx = @wallet.tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50)
117 124 @tx.out.last.value.should == 3950
118 125 end
119 126  
120 127 it "should send change to specified address" do
121   - @tx = @wallet.tx([['1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50,
  128 + @tx = @wallet.tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50,
122 129 '1EAntvSjkNeaJJTBQeQcN1ieU2mYf4wU9p')
123 130 Script.new(@tx.out.last.pk_script).get_address.should ==
124 131 '1EAntvSjkNeaJJTBQeQcN1ieU2mYf4wU9p'
125 132  
126 133  
127 134  
... ... @@ -128,20 +135,50 @@
128 135 key = Key.generate
129 136 @keystore.expect(:new_key, key)
130 137 @keystore.expect(:keys, [@key, key])
131   - @tx = @wallet.tx([['1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50, :new)
  138 + @tx = @wallet.tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 1000]], 50, :new)
132 139 @wallet.addrs.size.should == 2
133 140 @wallet.addrs.last.should == key.addr
134 141 Script.new(@tx.out.last.pk_script).get_address.should == key.addr
135 142 end
136 143  
137 144 it "should return nil if insufficient balance" do
138   - @tx = @wallet.tx([['1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 7000]])
  145 + @tx = @wallet.tx([[:address, '1M2JjkX7KAgwMyyF5xc2sPSfE7mL1jqkE7', 7000]])
139 146 @tx.should == nil
140 147 end
  148 +
141 149 end
142 150  
143   - # it "should send tx" # TODO
144   - # it "should update txouts on new tx" (txouts, balance, list) # TODO: node callbacks
  151 + describe "Bitcoin::Wallet::Wallet#tx (multisig)" do
  152 + def txout_mock(value, next_in)
  153 + txout = Mock.new
  154 + txout.expect(:value, value)
  155 + txout.expect(:get_next_in, next_in)
  156 + end
  157 +
  158 + before do
  159 + txout = txout_mock(5000, nil)
  160 + tx = Mock.new
  161 + tx.expect(:binary_hash, "foo")
  162 + tx.expect(:out, [txout])
  163 + txout.expect(:get_tx, tx)
  164 + txout.expect(:get_address, @addr)
  165 + txout.expect(:pk_script, Script.to_address_script(@addr))
  166 + @storage.expect(:get_txouts_for_address, [txout], [@addr])
  167 + @keystore.expect(:key, @key, [@addr])
  168 + selector = Mock.new
  169 + selector.expect(:select, [txout], [1000])
  170 + @selector.expect(:new, selector, [[txout]])
  171 + @tx = @wallet.tx([[:multisig, 1, @key.pub, @key2.pub, 1000]])
  172 + end
  173 +
  174 + it "should have correct outputs" do
  175 + @tx.out.size.should == 2
  176 + @tx.out.first.value.should == 1000
  177 + s = Script.new(@tx.out.first.pk_script)
  178 + s.get_addresses.should == [@addr, @addr2]
  179 + end
  180 +
  181 + end
145 182  
146 183 end