spec/bitcoin/node/command_api_spec.rb in bitcoin-ruby-0.0.5 vs spec/bitcoin/node/command_api_spec.rb in bitcoin-ruby-0.0.6

- old
+ new

@@ -1,18 +1,37 @@ require_relative '../spec_helper.rb' include Bitcoin include Builder +class Array + def stringify_keys + map do |e| + (e.is_a?(Array) || e.is_a?(Hash)) ? e.stringify_keys : e + end + end +end + +class Hash + def stringify_keys + Hash[map do |k, v| + v = v.stringify_keys if v.is_a?(Hash) || v.is_a?(Array) + [k.to_s, v] + end] + end +end + describe 'Node Command API' do - def test_command command, params = [], response = nil, &block + TSLB_TIMEOUT = 3 + + def test_command command, params = nil, response = nil, &block $responses = {} EM.run do @client = Bitcoin::Network::CommandClient.connect(*@config[:command]) do on_connected do - request(command, *params) + request(command, params) end on_response do |cmd, data| $responses[cmd] = data EM.stop end @@ -24,11 +43,11 @@ return result unless response || block if block block.call(result) else - result.should == response + raise "ERROR: #{result} != #{response}" unless result.should == response end end before do @@ -47,59 +66,58 @@ log: { network: :warn, storage: :warn }, } @node = Bitcoin::Network::Node.new(@config) @pid = fork do - $stdout = StringIO.new +# $stdout = StringIO.new SimpleCov.running = false if defined?(SimpleCov) @node.run end @genesis = P::Block.new("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000".htb) Bitcoin.network[:proof_of_work_limit] = Bitcoin.encode_compact_bits("ff"*32) @key = Bitcoin::Key.generate @block = create_block @genesis.hash, false, [], @key - test_command "store_block", [@genesis.to_payload.hth] + test_command "store_block", hex: @genesis.to_payload.hth sleep 0.1 + + @id = 0 end after do Process.kill("TERM", @pid) end it "should return error for unknown command" do - test_command("foo").should == {"error" => "unknown command: foo. send 'help' for help."} + test_command("foo", nil, {"error" => "unknown command: foo. send 'help' for help."}) end - it "should return error for wrong parameters" do - test_command("info", "foo").should == {"error" => "wrong number of arguments (1 for 0)"} - end + # it "should return error for wrong parameters" do + # test_command("info", "foo", {"error" => "wrong number of arguments (1 for 0)"}) + # end it "should query tslb" do - test_command("tslb") do |res| - res.keys.include?("tslb").should == true - res["tslb"].should >= 0 - res["tslb"].should <= 1 - end + test_command("tslb") {|r| (0..TSLB_TIMEOUT).include?(r['tslb']).should == true } end it "should query info" do info = test_command "info" info.is_a?(Hash).should == true - info["blocks"].should == "0 (?)" - info["addrs"].should == "0 (0)" - info["connections"].should == "0 established (0 out, 0 in), 0 connecting" + info["blocks"].should == { "depth" => 0, "peers" => "?", "sync" => false } + info["addrs"].should == { "alive" => 0, "total" => 0 } + info["connections"].should == { + "established" => 0, "outgoing" => 0, "incoming" => 0, "connecting" => 0 } info["queue"].should == 0 info["inv_queue"].should == 0 info["inv_cache"].should == 0 info["network"].should == "bitcoin" info["storage"].should == "sequel::sqlite:/" info["version"].should == 70001 info["external_ip"].should == "127.0.0.1" - info["uptime"].should =~ /00:00:00:0[0|1]/ + info["uptime"].between?(0, 1).should == true end it "should query config" do test_command("config").should == JSON.load(@node.config.to_json) end @@ -109,49 +127,64 @@ test_command("connections").should == [] end # TODO it "should connect" do - test_command("connect", ["127.0.0.1:1234"])["state"].should == "Connecting..." + test_command("connect", {host: "127.0.0.1", port: 1234})["state"].should == "connecting" end # TODO it "should disconnect" do - test_command("disconnect", ["127.0.0.1:1234"])["state"].should == "Disconnected" + test_command("disconnect", ["127.0.0.1:1234"])["state"].should == "disconnected" end it "should store block" do - test_command("info")["blocks"].should == "0 (?)" - res = test_command "store_block", [ @block.to_payload.hth ] - res.should == { "queued" => [ "block", @block.hash ] } + test_command("info")["blocks"].should == {"depth" => 0, "peers" => "?", "sync" => false} + res = test_command("store_block", { hex: @block.to_payload.hth }) + res.should == { "queued" => @block.hash } sleep 0.1 - test_command("info")["blocks"].should == "1 (?) sync" + test_command("info")["blocks"]["depth"].should == 1 + test_command("info")["blocks"]["sync"].should == true end + # TODO + # it "should store tx" do + # @tx = @block.tx[1] + # res = test_command("store_tx", { hex: @tx.to_payload.htb }) + # res.should == { "queued" => @tx.hash } + # end + describe :create_tx do before do @key2 = Key.generate - test_command("store_block", [@block.to_payload.hth]) + test_command("store_block", hex: @block.to_payload.hth) sleep 0.1 end it "should create transaction from given private keys" do - res = test_command("create_tx", [[@key.to_base58], [[@key2.addr, 10e8], [@key.addr, 40e8]]]) - tx = P::Tx.new(res[0].htb) - tx.is_a?(P::Tx).should == true + res = test_command("create_tx", { + keys: [ @key.to_base58 ], + recipients: [[@key2.addr, 10e8], [@key.addr, 40e8]] + }) + tx = P::Tx.new(res["hex"].htb) + tx.hash.should == res["hash"] tx.verify_input_signature(0, @block.tx[0]).should == true end - it "should create transaction from given addresses" do - res = test_command("create_tx", [[@key.addr], [[@key2.addr, 10e8], [@key.addr, 40e8]]]) - tx = P::Tx.new(res[0].htb) - tx.is_a?(P::Tx).should == true + it "should create unsigned transaction from given addresses" do + res = test_command("create_tx", { + keys: [ @key.addr ], + recipients: [[@key2.addr, 10e8], [@key.addr, 40e8]] + }) + tx = P::Tx.new(res["hex"].htb) + tx.hash.should == res["hash"] tx.in[0].script_sig.should == "" - -> { tx.verify_input_signature(0, @block.tx[0]) }.should.raise(TypeError) + #-> { tx.verify_input_signature(0, @block.tx[0]) }.should.raise(TypeError) + tx.verify_input_signature(0, @block.tx[0]).should == false - res[1].each.with_index do |sig_data, idx| + res["missing_sigs"].each.with_index do |sig_data, idx| sig_hash, sig_addr = *sig_data sig_addr.should == @key.addr sig = @key.sign(sig_hash.htb) script_sig = Script.to_signature_pubkey_script(sig, @key.pub.htb) tx.in[idx].script_sig_length = script_sig.bytesize @@ -160,16 +193,20 @@ tx.verify_input_signature(0, @block.tx[0]).should == true end it "should create transaction from given pubkeys" do - res = test_command("create_tx", [[@key.pub], [[@key2.addr, 10e8], [@key.addr, 40e8]]]) - tx = P::Tx.new(res[0].htb) - tx.is_a?(P::Tx).should == true - -> { tx.verify_input_signature(0, @block.tx[0]) }.should.raise(TypeError) + res = test_command("create_tx", { + keys: [@key.pub], + recipients: [[@key2.addr, 10e8], [@key.addr, 40e8]] + }) + tx = P::Tx.new(res["hex"].htb) + tx.hash.should == res["hash"] + #-> { tx.verify_input_signature(0, @block.tx[0]) }.should.raise(TypeError) + tx.verify_input_signature(0, @block.tx[0]).should == false - res[1].each.with_index do |sig_data, idx| + res["missing_sigs"].each.with_index do |sig_data, idx| sig_hash, sig_addr = *sig_data sig_addr.should == @key.addr sig = @key.sign(sig_hash.htb) script_sig = Script.to_signature_pubkey_script(sig, @key.pub.htb) tx.in[idx].script_sig_length = script_sig.bytesize @@ -190,56 +227,57 @@ i.prev_out_index 0 end t.output {|o| o.value 50e8; o.script {|s| s.recipient @key.addr } } end sig = @key.sign(tx.in[0].sig_hash) - test_command("store_block", [@block.to_payload.hth]) + test_command("store_block", hex: @block.to_payload.hth) sleep 0.1 - res = test_command("assemble_tx", [tx.to_payload.hth, [[sig.hth, @key.pub]]]) - tx = Bitcoin::P::Tx.new(res.htb) + res = test_command("assemble_tx", {tx: tx.to_payload.hth, sig_pubs: [[sig.hth, @key.pub]]}) + tx = Bitcoin::P::Tx.new(res["hex"].htb) + tx.hash.should == res["hash"] tx.verify_input_signature(0, @block.tx[0]).should == true end end describe :relay_tx do it "should handle decoding error" do - res = test_command("relay_tx", ["foobar"]) + res = test_command("relay_tx", hex: "foobar") res["error"].should == "Error decoding transaction." end it "should handle syntax error" do # create transaction with invalid output size block = create_block(@block.hash, false, [->(t) { create_tx(t, @block.tx[0], 0, [[22e14, @key]]) }], @key) tx = block.tx[1] - error = test_command("relay_tx", [tx.to_payload.hth]) + error = test_command("relay_tx", hex: tx.to_payload.hth) error["error"].should == "Transaction syntax invalid." error["details"].should == ["output_values", [22e14, 21e14]] end it "should handle context error" do # create transaction with invalid input block = create_block(@block.hash, false, [->(t) { create_tx(t, @block.tx[0], 0, [[25e8, @key]]) }], @key) tx = block.tx[1] - error = test_command("relay_tx", [tx.to_payload.hth]) + error = test_command("relay_tx", hex: tx.to_payload.hth) error["error"].should == "Transaction context invalid." error["details"].should == ["prev_out", [[@block.tx[0].hash, 0]]] end it "should relay transaction" do block = create_block(@block.hash, false, [->(t) { create_tx(t, @block.tx[0], 0, [[25e8, @key]]) }], @key) tx = block.tx[1] - test_command("store_block", [@block.to_payload.hth]) + test_command("store_block", hex: @block.to_payload.hth) sleep 0.1 - res = test_command("relay_tx", [tx.to_payload.hth, 1, 0]) + res = test_command("relay_tx", hex: tx.to_payload.hth, send: 1, wait: 0) res["success"].should == true res["hash"].should == tx.hash res["propagation"].should == { "sent" => 1, "received" => 0, "percent" => 0.0 } end @@ -248,126 +286,375 @@ describe :monitor do before do @client = TCPSocket.new(*@config[:command]) - def send data - @client.write(data.to_json + "\x00") + def send method, params = nil, client = @client + request = { id: @id += 1, method: method, params: params } + client.write(request.to_json + "\x00") + request.stringify_keys end - def should_receive expected - buf = "" - while b = @client.read(1) - break if b == "\x00" - buf << b + def should_receive request, expected, client = @client + expected = expected.stringify_keys if expected.is_a?(Hash) + begin + Timeout.timeout(100) do + buf = "" + while b = client.read(1) + break if b == "\x00" + buf << b + end + resp = JSON.load(buf) + expected = request.merge(result: expected).stringify_keys + expected.delete("params") + raise "ERROR: #{resp} != #{expected}" unless resp.should == expected + end + rescue Timeout::Error + print " [TIMEOUT]" + :timeout.should == nil end - resp = JSON.load(buf) - resp.should == expected end + def should_receive_block request, block, depth, client = @client + expected = { hash: block.hash, hex: block.to_payload.hth, depth: depth } + should_receive(request, expected, client) + end + + def should_receive_tx request, tx, conf, client = @client + expected = { hash: tx.hash, nhash: tx.nhash, hex: tx.to_payload.hth, conf: conf } + should_receive(request, expected, client) + end + + def should_receive_output request, tx, idx, conf, client = @client + expected = { hash: tx.hash, nhash: tx.nhash, idx: idx, + address: tx.out[idx].parsed_script.get_address, value: tx.out[idx].value, conf: conf } + should_receive(request, expected, client) + end + def store_block block - send ["store_block", [ block.to_payload.hth ]] - should_receive ["store_block", {"queued" => [ "block", block.hash ]}] + request = send("store_block", hex: block.to_payload.hth) + should_receive(request, {"queued" => block.hash }) end end + describe :channels do + + it "should combine multiple channels" do + should_receive r1 = send("monitor", channel: "block"), id: 0 + should_receive r2 = send("monitor", channel: "tx", conf: 1), id: 1 + store_block @block + should_receive_block(r1, @block, 1) + should_receive_tx(r2, @block.tx[0], 1) + end + + it "should handle multiple clients" do + @client2 = TCPSocket.new(*@config[:command]) + should_receive r1_1 = send("monitor", channel: "tx", conf: 1), id: 0 + r1_2 = send("monitor", { channel: "block" }, @client2) + should_receive r1_2, { id: 0 }, @client2 + + store_block @block + + should_receive_block(r1_2, @block, 1, @client2) + should_receive_tx(r1_1, @block.tx[0], 1) + + block = create_block @block.hash, false + store_block block + + should_receive_block(r1_2, block, 2, @client2) + should_receive_tx(r1_1, block.tx[0], 1) + + r2_2 = send "monitor", { channel: "tx", conf: 1 }, @client2 + should_receive r2_2, { id: 1 }, @client2 + should_receive r2_1 = send("monitor", channel: "block"), id: 1 + + block = create_block block.hash, false + store_block block + + should_receive_block(r1_2, block, 3, @client2) + should_receive_tx(r2_2, block.tx[0], 1, @client2) + + should_receive_tx(r1_1, block.tx[0], 1) + + # if something was wrong, we would now receive the last tx again + + should_receive_block(r2_1, block, 3) + + block = create_block block.hash, false + store_block block + + should_receive_tx(r1_1, block.tx[0], 1) + + should_receive_block(r2_1, block, 4) + should_receive_block(r1_2, block, 4, @client2) + should_receive_tx(r2_2, block.tx[0], 1, @client2) + end + + end + describe :block do before do - send ["monitor", ["block"]] - should_receive ["monitor", ["block", [ @genesis.to_hash, 0 ]]] + @request = send "monitor", channel: "block" + + should_receive(@request, id: 0) store_block @block - should_receive ["monitor", ["block", [ @block.to_hash, 1 ]]] + should_receive_block(@request, @block, 1) end it "should monitor block" do @block = create_block @block.hash, false store_block @block - should_receive ["monitor", ["block", [ @block.to_hash, 2 ]]] + should_receive_block(@request, @block, 2) end + it "should unmonitor block" do + @request = send "unmonitor", id: 0 + should_receive @request, id: 0 + store_block create_block(@block.hash, false) + test_command("tslb") {|r| (0..TSLB_TIMEOUT).include?(r['tslb']).should == true } + end + it "should not monitor side or orphan blocks" do @side = create_block @genesis.hash, false store_block @side @orphan = create_block "00" * 32, false store_block @orphan # should not send side or orphan block only the next main block @block = create_block @block.hash, false store_block @block - should_receive ["monitor", ["block", [ @block.to_hash, 2 ]]] + + should_receive_block(@request, @block, 2) end + it "should received missed blocks when last block hash is given" do + @client = TCPSocket.new(*@config[:command]) + blocks = [@block] + 3.times do + blocks << create_block(blocks.last.hash, false) + store_block blocks.last + end + sleep 0.1 + + r = send "monitor", channel: "block", last: blocks[1].hash + + should_receive_block(r, blocks[1], 2) + should_receive_block(r, blocks[2], 3) + should_receive_block(r, blocks[3], 4) + end + end + describe :reorg do + + before do + @request = send "monitor", channel: "reorg" + should_receive @request, id: 0 + store_block @block + end + + it "should monitor reorg" do + @block1 = create_block @genesis.hash, false + store_block @block1 + @block2 = create_block @block1.hash, false + store_block @block2 + should_receive @request, { new_main: [ @block1.hash ], new_side: [ @block.hash ] } + end + + it "should unmonitor reorg" do + r = send "unmonitor", id: 0 + should_receive r, id: 0 + @block1 = create_block @genesis.hash, false + store_block @block1 + @block2 = create_block @block1.hash, false + store_block @block2 + + test_command("tslb") {|r| (0..TSLB_TIMEOUT).include?(r['tslb']).should == true } + end + + end + describe :tx do + it "should monitor unconfirmed tx" do - send ["monitor", ["tx"]] + r1 = send "monitor", channel: "tx" + should_receive r1, id: 0 tx = @block.tx[0] - send ["store_tx", [ tx.to_payload.hth ] ] - should_receive ["store_tx", { "queued" => [ "tx", tx.hash ]}] - should_receive ["monitor", ["tx", [ tx.to_hash, 0 ]]] + r2 = send "store_tx", hex: tx.to_payload.hth + should_receive r2, { "queued" => tx.hash } + + should_receive_tx(r1, tx, 0) end + it "should unmonitor tx" do + r1 = send "monitor", channel: "tx" + should_receive r1, id: 0 + + r2 = send "unmonitor", id: 0 + should_receive r2, id: 0 + + tx = @block.tx[0] + r3 = send "store_tx", hex: tx.to_payload.hth + should_receive r3, { "queued" => tx.hash } + + test_command("tslb") {|r| (0..TSLB_TIMEOUT).include?(r['tslb']).should == true } + end + it "should monitor confirmed tx" do - send ["monitor", ["tx_1"]] + r = send "monitor", channel: "tx", conf: 1 + should_receive r, id: 0 store_block @block - should_receive ["monitor", ["tx_1", [ @block.tx[0].to_hash, 1 ]]] + + should_receive_tx(r, @block.tx[0], 1) end it "should monitor tx for given confirmation level" do - send ["monitor", ["tx_3"]] + r = send "monitor", channel: "tx", conf: 3 + should_receive r, id: 0 + @tx = @block.tx[0] store_block @block @block = create_block @block.hash, false store_block @block - should_receive ["monitor", ["tx_3", [ @genesis.tx[0].to_hash, 3 ]]] + + should_receive_tx(r, @genesis.tx[0], 3) + @block = create_block @block.hash, false store_block @block - should_receive ["monitor", ["tx_3", [ @tx.to_hash, 3 ]]] + + should_receive_tx(r, @tx, 3) end + it "should receive missed txs when last txhash is given" do + @client = TCPSocket.new(*@config[:command]) + blocks = [@block]; store_block @block + 3.times do + blocks << create_block(blocks.last.hash, false) + store_block blocks.last + end + sleep 0.1 + + r = send "monitor", channel: "tx", conf: 1, last: blocks[0].tx[0].hash + + should_receive_tx(r, blocks[1].tx[0], 3) + should_receive_tx(r, blocks[2].tx[0], 2) + should_receive_tx(r, blocks[3].tx[0], 1) + + should_receive r, id: 0 + end + + + it "should filter txs for given addresses" do + @key2 = Bitcoin::Key.generate + block = create_block(@block.hash, false, [->(t) { + create_tx(t, @block.tx[0], 0, [[50e8, @key2]]) }], @key) + @addr = @block.tx[0].out[0].parsed_script.get_address + r = send "monitor", channel: "tx", conf: 1, addresses: [ @key2.addr ] + should_receive r, id: 0 + store_block @block + store_block block + should_receive_tx(r, block.tx[1], 1) + end + end describe :output do before do @tx = @block.tx[0]; @out = @tx.out[0] - @addr = Bitcoin::Script.new(@out.pk_script).get_address end it "should monitor unconfirmed outputs" do - send ["monitor", ["output"]] + r1 = send "monitor", channel: "output" + should_receive r1, id: 0 tx = @block.tx[0] - send ["store_tx", [ tx.to_payload.hth ]] - should_receive ["store_tx", { "queued" => [ "tx", tx.hash ]}] - addr = Bitcoin::Script.new(tx.out[0].pk_script).get_address - should_receive ["monitor", ["output", [ tx.hash, addr, tx.out[0].value, 0]]] + r2 = send "store_tx", hex: tx.to_payload.hth + should_receive r2, { "queued" => tx.hash } + should_receive_output(r1, tx, 0, 0) end + it "should unmonitor outputs" do + should_receive send("monitor", channel: "output"), id: 0 + should_receive send("unmonitor", id: 0), id: 0 + + tx = @block.tx[0] + r2 = send "store_tx", hex: tx.to_payload.hth + should_receive r2, { "queued" => tx.hash } + + test_command("tslb") {|r| (0..TSLB_TIMEOUT).include?(r['tslb']).should == true } + end + it "should monitor confirmed output" do - send ["monitor", ["output_1"]] + r = send "monitor", channel: "output", conf: 1 + should_receive r, id: 0 store_block @block - should_receive ["monitor", ["output_1", [ @tx.hash, @addr, @out.value, 1 ]]] + should_receive_output(r, @tx, 0, 1) end it "should monitor output for given confirmation level" do - send ["monitor", ["output_3"]] + r = send "monitor", channel: "output", conf: 3 + should_receive r, id: 0 store_block @block @block = create_block @block.hash, false store_block @block tx = @genesis.tx[0]; out = tx.out[0] - addr = Bitcoin::Script.new(out.pk_script).get_address - should_receive ["monitor", ["output_3", [ tx.hash, addr, out.value, 3 ]]] + should_receive_output(r, tx, 0, 3) @block = create_block @block.hash, false store_block @block - should_receive ["monitor", ["output_3", [ @tx.hash, @addr, @out.value, 3 ]]] - + should_receive_output(r, @tx, 0, 3) + end + + it "should receive missed outputs when last txhash:idx is given" do + @key = Bitcoin::Key.generate + @client = TCPSocket.new(*@config[:command]) + blocks = [@block]; store_block @block + 3.times do + blocks << create_block(blocks.last.hash, false, [], @key) + store_block blocks.last + end + sleep 0.1 + + r = send "monitor", channel: "output", conf: 1, last: "#{blocks[0].tx[0].hash}:0" + + should_receive_output(r, blocks[1].tx[0], 0, 3) + should_receive_output(r, blocks[2].tx[0], 0, 2) + should_receive_output(r, blocks[3].tx[0], 0, 1) + + should_receive r, id: 0 + end + + it "should filter outputs for given addresses" do + @key2 = Bitcoin::Key.generate + block = create_block(@block.hash, false, [->(t) { + create_tx(t, @block.tx[0], 0, [[50e8, @key2]]) }], @key) + + r = send "monitor", channel: "output", conf: 1, addresses: [ @key2.addr ] + should_receive r, id: 0 + store_block @block + store_block block + should_receive_output(r, block.tx[1], 0, 1) + end + + it "should add filter address to output monitor params" do + @key2 = Bitcoin::Key.generate + block = create_block(@block.hash, false, [->(t) { + create_tx(t, @block.tx[0], 0, [[50e8, @key2]]) }], @key) + + r1 = send "monitor", channel: "output", conf: 1, addresses: [ ] + should_receive r1, id: 0 + + r2 = send "filter_monitor_output", id: 0, address: @key2.addr + should_receive r2, id: 0 + + store_block @block + store_block block + should_receive_output(r1, block.tx[1], 0, 1) end end end