Commit 624abdbcd52461ff6375422cfd5f026e14ca2a2e

Authored by comboy
1 parent 611b804a7f
Exists in master and in 1 other branch benchmark

fake blockchain generator + some simple performance test

Showing 5 changed files with 231 additions and 2 deletions Side-by-side Diff

... ... @@ -9,6 +9,7 @@
9 9 log/
10 10 rdoc/
11 11 coverage/
  12 +spec/bitcoin/fixtures/fake_chain
12 13 *.conf
13 14 *.db
14 15 .rbx/
lib/bitcoin/builder.rb
... ... @@ -71,6 +71,7 @@
71 71 def tx tx = nil
72 72 tx ||= ( c = TxBuilder.new; yield c; c.tx )
73 73 @block.tx << tx
  74 + tx
74 75 end
75 76  
76 77 # create the block according to values specified via DSL.
spec/bitcoin/helpers/fake_blockchain.rb
  1 +require_relative '../spec_helper'
  2 +require 'fileutils'
  3 +
  4 +Bitcoin::NETWORKS[:fake] = {
  5 + :project => :bitcoin,
  6 + :magic_head => "fake",
  7 + :address_version => "00",
  8 + :p2sh_version => "05",
  9 + :privkey_version => "80",
  10 + :default_port => 78333,
  11 + :coinbase_maturity => 0,
  12 + :protocol_version => 70001,
  13 + :max_money => 21_000_000 * 100_000_000,
  14 + :dns_seeds => [],
  15 + :genesis_hash => "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
  16 + :proof_of_work_limit => 553713663,
  17 + :alert_pubkeys => [],
  18 + :known_nodes => [],
  19 + :checkpoints => {},
  20 + :min_tx_fee => 10_000,
  21 + :min_relay_tx_fee => 10_000,
  22 +}
  23 +
  24 +class Bitcoin::Validation::Block; def difficulty; true; end; end
  25 +
  26 +
  27 +# Small utility to generate fake blocks mostly to be able to test performance
  28 +# They are full from the start, so that we don't have to import 100K blocks to check
  29 +# how performance looks when storing or validating 1K transactions
  30 +class FakeBlockchain
  31 +
  32 + # Generate fake blockchain with +num+ number of blocks
  33 + # Blocks are provided as an argument to the block given to the method
  34 + # E.g.
  35 + # FakeBlockChain.generate(5) {|b| save_block(b) }
  36 + def self.generate(num = 50, opts = {})
  37 + Bitcoin.network = :fake
  38 + srand 1337
  39 +
  40 + default_opts = {
  41 + block_size: 950_000, # minimum block size
  42 + num_keys: 1000, # number of different keys being used
  43 + genesis_timestamp: Time.new(2009).to_i,
  44 + verbose: true, # print out debug information
  45 + }
  46 +
  47 + opts = default_opts.merge(opts)
  48 +
  49 + to_spend = [] # table of outputs that we can spend
  50 + lost_count = 0 # keeping track of lost coins
  51 + keys = Array.new(opts[:num_keys]) { Bitcoin::Key.generate }
  52 + timestamp = opts[:genesis_timestamp]
  53 +
  54 + genesis = Bitcoin::Builder.build_block do |blk|
  55 + blk.time timestamp
  56 + blk.prev_block "00"*32
  57 + blk.tx do |t|
  58 + t.input {|i| i.coinbase }
  59 + t.output {|o| o.value 50*Bitcoin::COIN; o.script {|s| s.recipient keys[0].addr } }
  60 + end
  61 + end
  62 + Bitcoin.network[:genesis_hash] = genesis.hash
  63 +
  64 + to_spend << {tx: genesis.tx[0], tx_idx: 0, key: keys[0], value: 50e8}
  65 +
  66 + prev_block = genesis
  67 +
  68 +
  69 + num.times do |blk_i|
  70 +
  71 + timestamp += 600
  72 + t0 = Time.now
  73 +
  74 + block = Bitcoin::Builder.build_block do |blk|
  75 + blk.time timestamp
  76 + blk.prev_block prev_block.hash
  77 + key0 = keys.sample
  78 + tx0 = blk.tx do |t|
  79 + t.input {|i| i.coinbase }
  80 + t.output {|o| o.value 50e8; o.script {|s| s.recipient key0.addr } }
  81 + end
  82 +
  83 + # We "lose" some coins, that is we decide never to spend some outputs
  84 + # It's to keep utxo growing without making block generation time growing
  85 + lost_count += to_spend.size
  86 + to_spend = to_spend.reject.with_index {|x,i| i==0 ? false : (((to_spend.size - i) / to_spend.size.to_f)**2 * rand > rand*0.2) }
  87 + lost_count -= to_spend.size
  88 +
  89 + # many txs vs many tx outputs in given block
  90 + many_outputs_prob = 0.5 * (rand ** 3)
  91 +
  92 + total_tx_size = 0
  93 +
  94 + # generate tranasctions
  95 + loop do
  96 + # we want utxo to keep growing so we use many inputs only with some small probability
  97 + ins = to_spend[(rand(to_spend.size)).to_i..-1].sample(rand < 0.01 ? (rand(50) + 1) : 1)
  98 + total = ins.map{|i| i[:value]}.inject(:+)
  99 + next if total < 20_000
  100 +
  101 + new_outs = []
  102 +
  103 + tx = blk.tx do |t|
  104 +
  105 + # generate inputs
  106 + ins.map do |input|
  107 + t.input do |i|
  108 + i.prev_out input[:tx]
  109 + i.prev_out_index input[:tx_idx]
  110 + i.signature_key input[:key]
  111 + end
  112 + end
  113 + # remove outputs that we just used
  114 + ins.each {|i| to_spend.delete i}
  115 +
  116 + fee = 10_000
  117 + # helper to split value randomly in a half
  118 + half_split = ->(v) { split = [rand, 0.1].max; [v*split, v*(1-split)] }
  119 + # helper to split value randomly to many pieces
  120 + value_split = ->(v, depth=0) {(depth < 10 && rand > 0.2) ? half_split[v].map{|x| value_split[x, depth+1]} : [v] }
  121 +
  122 + if rand < many_outputs_prob
  123 + # every now and then there are many outptus
  124 + out_values = value_split[total-fee].flatten.map {|x| x.round(8)}
  125 + out_values.each.with_index do |v,i|
  126 + key = keys.sample
  127 + t.output {|o| o.value v; o.script {|s| s.recipient key.addr }}
  128 + new_outs << {tx_idx: i, key: key, value: v}
  129 + end
  130 + else
  131 + # most txs seem to have 2 outputs
  132 + k1 = keys.sample
  133 + k2 = keys.sample
  134 + v1, v2 = half_split[total-fee]
  135 + t.output {|o| o.value v1; o.script {|s| s.recipient k1.addr }}
  136 + t.output {|o| o.value v2; o.script {|s| s.recipient k2.addr }}
  137 + new_outs << {tx_idx: 0, key: k1, value: v2}
  138 + new_outs << {tx_idx: 1, key: k2, value: v2}
  139 + end
  140 + end
  141 +
  142 + new_outs.each {|o| to_spend << {tx: tx}.merge(o) } # fun fact: the opposite merge is way slower
  143 +
  144 + total_tx_size += tx.to_payload.size
  145 + break if total_tx_size > opts[:block_size]
  146 + end
  147 +
  148 + # coinbase
  149 + to_spend << {tx: tx0, tx_idx: 0, key: key0, value: 50e8}
  150 + end
  151 + puts "depth #{blk_i}/#{num} \t txcount: #{block.tx.size} \t size: #{block.to_payload.size} \t utxo count: #{to_spend.size + lost_count} (#{to_spend.size}) \t ttg: #{'%.2f' % (Time.now - t0)}s" if opts[:verbose]
  152 + yield(block)
  153 + prev_block = block
  154 + end
  155 + true
  156 + end
  157 +
  158 + def self.prepare
  159 + Bitcoin.network = :fake
  160 + if File.exist? block_path(0)
  161 + genesis = Bitcoin::P::Block.new File.read block_path 0
  162 + Bitcoin.network[:genesis_hash] = genesis.hash
  163 + else
  164 + STDERR.puts "\nFake blockchain not present, generating (go take a nap)..."
  165 + depth = 0
  166 + FileUtils.mkdir_p fixtures_path "fake_chain"
  167 + generate(50) do |blk|
  168 + File.open(block_path(depth),'w') {|f| f.write blk.to_payload }
  169 + depth += 1
  170 + end
  171 + end
  172 + end
  173 +
  174 + def self.block(depth)
  175 + Bitcoin::Protocol::Block.new File.read block_path depth
  176 + end
  177 +
  178 + def self.block_path(depth)
  179 + fixtures_path "fake_chain/#{depth}.blk"
  180 + end
  181 +
  182 +end
spec/bitcoin/performance/storage_spec.rb
  1 +# encoding: ascii-8bit
  2 +
  3 +require_relative '../spec_helper'
  4 +require_relative '../helpers/fake_blockchain'
  5 +require 'benchmark'
  6 +
  7 +[
  8 + [:sequel, :postgres]
  9 +].compact.each do |options|
  10 +
  11 + next unless storage = setup_db(*options)
  12 +
  13 + describe "#{storage.backend_name} block storage" do
  14 +
  15 + before do
  16 + @store = storage
  17 + @store.reset
  18 + @store.log.level = :error
  19 + class Bitcoin::Validation::Block; def difficulty; true; end; end
  20 +
  21 + FakeBlockchain.prepare
  22 + end
  23 +
  24 + it "block storage" do
  25 +
  26 + bm = Benchmark.measure do
  27 + bm = Benchmark.bm do |b|
  28 + 10.times do |i|
  29 + b.report("storing fake block ##{i}") { @store.new_block FakeBlockchain.block(i) }
  30 + end
  31 + end
  32 + end
  33 + puts '-'*80
  34 + puts "TOTAL #{bm.format}"
  35 +
  36 + should.satisfy { "human" }
  37 + end
  38 +
  39 +
  40 + end
  41 +end
spec/bitcoin/spec_helper.rb
... ... @@ -20,10 +20,14 @@
20 20  
21 21 require 'bitcoin'
22 22  
  23 +def fixtures_path(relative_path)
  24 + File.join(File.dirname(__FILE__), 'fixtures', relative_path)
  25 +end
  26 +
23 27 def fixtures_file(relative_path)
24   - basedir = File.join(File.dirname(__FILE__), 'fixtures')
25   - Bitcoin::Protocol.read_binary_file( File.join(basedir, relative_path) )
  28 + Bitcoin::Protocol.read_binary_file( fixtures_path(relative_path) )
26 29 end
  30 +
27 31  
28 32 include Bitcoin::Builder
29 33