bitcoin-ruby 0.0.5 → 0.0.6

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.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +2 -2
  4. data/COPYING +1 -1
  5. data/Gemfile +5 -11
  6. data/README.rdoc +11 -5
  7. data/Rakefile +5 -0
  8. data/bin/bitcoin_node +11 -29
  9. data/bin/bitcoin_node_cli +81 -0
  10. data/bin/bitcoin_wallet +9 -6
  11. data/doc/NODE.rdoc +79 -26
  12. data/examples/bbe_verify_tx.rb +1 -1
  13. data/examples/index_nhash.rb +24 -0
  14. data/examples/reindex_p2sh_addrs.rb +44 -0
  15. data/lib/bitcoin.rb +135 -20
  16. data/lib/bitcoin/builder.rb +233 -63
  17. data/lib/bitcoin/key.rb +89 -16
  18. data/lib/bitcoin/litecoin.rb +13 -11
  19. data/lib/bitcoin/namecoin.rb +5 -4
  20. data/lib/bitcoin/network/command_client.rb +23 -13
  21. data/lib/bitcoin/network/command_handler.rb +336 -131
  22. data/lib/bitcoin/network/connection_handler.rb +14 -13
  23. data/lib/bitcoin/network/node.rb +61 -20
  24. data/lib/bitcoin/protocol.rb +5 -1
  25. data/lib/bitcoin/protocol/block.rb +15 -3
  26. data/lib/bitcoin/protocol/parser.rb +3 -3
  27. data/lib/bitcoin/protocol/tx.rb +82 -20
  28. data/lib/bitcoin/protocol/txin.rb +7 -0
  29. data/lib/bitcoin/protocol/txout.rb +12 -9
  30. data/lib/bitcoin/script.rb +329 -75
  31. data/lib/bitcoin/storage/dummy/dummy_store.rb +23 -4
  32. data/lib/bitcoin/storage/models.rb +6 -11
  33. data/lib/bitcoin/storage/sequel/migrations/005_change_tx_hash_to_bytea.rb +14 -0
  34. data/lib/bitcoin/storage/sequel/migrations/006_add_tx_nhash.rb +31 -0
  35. data/lib/bitcoin/storage/sequel/migrations/007_add_prev_out_index_index.rb +16 -0
  36. data/lib/bitcoin/storage/sequel/migrations/008_add_txin_p2sh_type.rb +31 -0
  37. data/lib/bitcoin/storage/sequel/migrations/009_add_addrs_type.rb +56 -0
  38. data/lib/bitcoin/storage/sequel/sequel_store.rb +168 -70
  39. data/lib/bitcoin/storage/storage.rb +161 -97
  40. data/lib/bitcoin/storage/utxo/migrations/002_utxo.rb +1 -1
  41. data/lib/bitcoin/storage/utxo/migrations/004_add_addrs_type.rb +14 -0
  42. data/lib/bitcoin/storage/utxo/utxo_store.rb +25 -12
  43. data/lib/bitcoin/validation.rb +87 -56
  44. data/lib/bitcoin/version.rb +1 -1
  45. data/spec/bitcoin/bitcoin_spec.rb +38 -0
  46. data/spec/bitcoin/builder_spec.rb +177 -0
  47. data/spec/bitcoin/fixtures/litecoin-tx-f5aa30f574e3b6f1a3d99c07a6356ba812aabb9661e1d5f71edff828cbd5c996.json +259 -0
  48. data/spec/bitcoin/fixtures/rawblock-testnet-265322.bin +0 -0
  49. data/spec/bitcoin/fixtures/tx-0295028ef826b2a188409cb905b631faebb9bb3cdf14510571c5f4bd8591338f.json +64 -0
  50. data/spec/bitcoin/fixtures/tx-03339a725007a279484fb6f5361f522dd1cf4d0923d30e6b973290dba4275f92.json +64 -0
  51. data/spec/bitcoin/fixtures/tx-0ce7e5238fbdb6c086cf1b384b21b827e91cc23f360417265874a5a0d86ce367.json +64 -0
  52. data/spec/bitcoin/fixtures/tx-0ef34c49f630aea17df0080728b0fc67bf5f87fbda936934a4b11b4a69d7821e.json +64 -0
  53. data/spec/bitcoin/fixtures/tx-1129d2a8bd5bb3a81e54dc96a90f1f6b2544575748caa17243470935c5dd91b7.json +28 -0
  54. data/spec/bitcoin/fixtures/tx-19aa42fee0fa57c45d3b16488198b27caaacc4ff5794510d0c17f173f05587ff.json +23 -0
  55. data/spec/bitcoin/fixtures/tx-1a4f3b9dc4494aeedeb39f30dd37e60541b2abe3ed4977992017cc0ad4f44956.json +64 -0
  56. data/spec/bitcoin/fixtures/tx-1f9191dcf2b1844ca28c6ef4b969e1d5fab70a5e3c56b7007949e55851cb0c4f.json +64 -0
  57. data/spec/bitcoin/fixtures/tx-22cd5fef23684d7b304e119bedffde6f54538d3d54a5bfa237e20dc2d9b4b5ad.json +64 -0
  58. data/spec/bitcoin/fixtures/tx-2958fb00b4fd6fe0353503b886eb9a193d502f4fd5fc042d5e03216ba918bbd6.json +64 -0
  59. data/spec/bitcoin/fixtures/tx-29f277145749ad6efbed3ae6ce301f8d33c585ec26b7c044ad93c2f866e9e942.json +64 -0
  60. data/spec/bitcoin/fixtures/tx-2c5e5376c20e9cc78d0fb771730e5d840cc2096eff0ef045b599fe92475ace1c.json +28 -0
  61. data/spec/bitcoin/fixtures/tx-2c63aa814701cef5dbd4bbaddab3fea9117028f2434dddcdab8339141e9b14d1.json +30 -0
  62. data/spec/bitcoin/fixtures/tx-326882a7f22b5191f1a0cc9962ca4b878cd969cf3b3a70887aece4d801a0ba5e.json +23 -0
  63. data/spec/bitcoin/fixtures/tx-345bed8785c3282a264ffb0dbee61cde54854f10e16f1b3e75b7f2d9f62946f2.json +64 -0
  64. data/spec/bitcoin/fixtures/tx-39ba7440b7103557560cc8ce258009936796485aaf8b478e66ab4cb97c66e31b.json +32 -0
  65. data/spec/bitcoin/fixtures/tx-3a04d57a833367f1655cc5ec3beb587888ef4977a86caa8c8ad4ba7cc717eae7.json +64 -0
  66. data/spec/bitcoin/fixtures/tx-4142ee4877eb116abf955a7ec6ef2dc38133b793df762b76d75e3d7d4d8badc9.json +38 -0
  67. data/spec/bitcoin/fixtures/tx-46224764c7870f95b58f155bce1e38d4da8e99d42dbb632d0dd7c07e092ee5aa.json +23 -0
  68. data/spec/bitcoin/fixtures/tx-5df1375ffe61ac35ca178ebb0cab9ea26dedbd0e96005dfcee7e379fa513232f.json +30 -0
  69. data/spec/bitcoin/fixtures/tx-62d9a565bd7b5344c5352e3e9e5f40fa4bbd467fa19c87357216ec8777ba1cce.json +64 -0
  70. data/spec/bitcoin/fixtures/tx-6327783a064d4e350c454ad5cd90201aedf65b1fc524e73709c52f0163739190.json +23 -0
  71. data/spec/bitcoin/fixtures/tx-6606c366a487bff9e412d0b6c09c14916319932db5954bf5d8719f43f828a3ba.json +27 -0
  72. data/spec/bitcoin/fixtures/tx-6aaf18b9f1283b939d8e5d40ff5f8a435229f4178372659cc3a0bce4e262bf78.json +28 -0
  73. data/spec/bitcoin/fixtures/tx-6b48bba6f6d2286d7ec0883c0fc3085955090813a4c94980466611c798b868cc.json +64 -0
  74. data/spec/bitcoin/fixtures/tx-70cfbc6690f9ab46712db44e3079ac227962b2771a9341d4233d898b521619ef.json +40 -0
  75. data/spec/bitcoin/fixtures/tx-7a1a9db42f065f75110fcdb1bc415549c8ef7670417ba1d35a67f1b8adc562c1.json +64 -0
  76. data/spec/bitcoin/fixtures/tx-9a768fc7d0c4bdc86e25154357ef7c0063ca21310e5740a2f12f90b7455184a7.json +64 -0
  77. data/spec/bitcoin/fixtures/tx-9cad8d523a0694f2509d092c39cebc8046adae62b4e4297102d568191d9478d8.json +64 -0
  78. data/spec/bitcoin/fixtures/tx-9e052eb694bd7e15906433f064dff0161a12fd325c1124537766377004023c6f.json +64 -0
  79. data/spec/bitcoin/fixtures/tx-a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944.json +23 -0
  80. data/spec/bitcoin/fixtures/tx-aab7ef280abbb9cc6fbaf524d2645c3daf4fcca2b3f53370e618d9cedf65f1f8.json +23 -0
  81. data/spec/bitcoin/fixtures/tx-ab9805c6d57d7070d9a42c5176e47bb705023e6b67249fb6760880548298e742.json +27 -0
  82. data/spec/bitcoin/fixtures/tx-ad4bcf3241e5d2ad140564e20db3567d41594cf4c2012433fe46a2b70e0d87b8.json +64 -0
  83. data/spec/bitcoin/fixtures/tx-b5b598de91787439afd5938116654e0b16b7a0d0f82742ba37564219c5afcbf9.json +27 -0
  84. data/spec/bitcoin/fixtures/tx-b8fd633e7713a43d5ac87266adc78444669b987a56b3a65fb92d58c2c4b0e84d.json +28 -0
  85. data/spec/bitcoin/fixtures/tx-bbca0628c42cb8bf7c3f4b2ad688fa56da5308dd2a10255da89fb1f46e6e413d.json +36 -0
  86. data/spec/bitcoin/fixtures/tx-bc7fd132fcf817918334822ee6d9bd95c889099c96e07ca2c1eb2cc70db63224.json +23 -0
  87. data/spec/bitcoin/fixtures/tx-c192b74844e4837a34c4a5a97b438f1c111405b01b99e2d12b7c96d07fc74c04.json +28 -0
  88. data/spec/bitcoin/fixtures/tx-e335562f7e297aadeed88e5954bc4eeb8dc00b31d829eedb232e39d672b0c009.json +406 -0
  89. data/spec/bitcoin/fixtures/tx-eb3b82c0884e3efa6d8b0be55b4915eb20be124c9766245bcc7f34fdac32bccb.json +35 -0
  90. data/spec/bitcoin/fixtures/tx-fee1b9b85531c8fb6cd7831f83490c7f2aa768b6eefe29854ef5e89ce7b9ecb1.json +64 -0
  91. data/spec/bitcoin/fixtures/txscript-invalid-too-many-sigops-followed-by-invalid-pushdata.bin +1 -0
  92. data/spec/bitcoin/helpers/fake_blockchain.rb +183 -0
  93. data/spec/bitcoin/key_spec.rb +79 -8
  94. data/spec/bitcoin/namecoin_spec.rb +1 -1
  95. data/spec/bitcoin/node/command_api_spec.rb +373 -86
  96. data/spec/bitcoin/performance/storage_spec.rb +41 -0
  97. data/spec/bitcoin/protocol/addr_spec.rb +7 -5
  98. data/spec/bitcoin/protocol/aux_pow_spec.rb +1 -0
  99. data/spec/bitcoin/protocol/block_spec.rb +6 -0
  100. data/spec/bitcoin/protocol/tx_spec.rb +184 -1
  101. data/spec/bitcoin/protocol/txin_spec.rb +27 -0
  102. data/spec/bitcoin/protocol/txout_spec.rb +27 -0
  103. data/spec/bitcoin/script/opcodes_spec.rb +74 -3
  104. data/spec/bitcoin/script/script_spec.rb +271 -0
  105. data/spec/bitcoin/spec_helper.rb +34 -6
  106. data/spec/bitcoin/storage/models_spec.rb +104 -0
  107. data/spec/bitcoin/storage/reorg_spec.rb +42 -11
  108. data/spec/bitcoin/storage/storage_spec.rb +58 -15
  109. data/spec/bitcoin/storage/validation_spec.rb +44 -14
  110. data/spec/bitcoin/wallet/keygenerator_spec.rb +6 -3
  111. data/spec/bitcoin/wallet/keystore_spec.rb +3 -3
  112. data/spec/bitcoin/wallet/wallet_spec.rb +87 -89
  113. metadata +117 -11
@@ -72,6 +72,12 @@ module Bitcoin::Storage::Backends
72
72
  wrap_block(@blk.find {|blk| blk.tx.map(&:hash).include?(tx_hash) })
73
73
  end
74
74
 
75
+ def get_idx_from_tx_hash(tx_hash)
76
+ return nil unless tx = get_tx(tx_hash)
77
+ return nil unless blk = tx.get_block
78
+ blk.tx.index tx
79
+ end
80
+
75
81
  def get_tx(tx_hash)
76
82
  transaction = @tx[tx_hash]
77
83
  return nil unless transaction
@@ -88,15 +94,24 @@ module Bitcoin::Storage::Backends
88
94
  wrap_txin(txin)
89
95
  end
90
96
 
97
+ def get_txout_for_txin(txin)
98
+ return nil unless tx = @tx[txin.prev_out.reverse_hth]
99
+ wrap_tx(tx).out[txin.prev_out_index]
100
+ end
101
+
91
102
  def get_txouts_for_pk_script(script)
92
103
  txouts = @tx.values.map(&:out).flatten.select {|o| o.pk_script == script}
93
104
  txouts.map {|o| wrap_txout(o) }
94
105
  end
95
106
 
96
- def get_txouts_for_hash160(hash160, unconfirmed = false)
107
+ def get_txouts_for_hash160(hash160, type = :hash160, unconfirmed = false)
97
108
  @tx.values.map(&:out).flatten.map {|o|
98
109
  o = wrap_txout(o)
99
- o.hash160 == hash160 ? o : nil
110
+ if o.parsed_script.is_multisig?
111
+ o.parsed_script.get_multisig_pubkeys.map{|pk| Bitcoin.hash160(pk.unpack("H*")[0])}.include?(hash160) ? o : nil
112
+ else
113
+ o.hash160 == hash160 && o.type == type ? o : nil
114
+ end
100
115
  }.compact
101
116
  end
102
117
 
@@ -143,8 +158,7 @@ module Bitcoin::Storage::Backends
143
158
  def wrap_txout(output)
144
159
  return nil unless output
145
160
  tx = @tx.values.find{|t| t.out.include?(output)}
146
- data = {tx_id: tx.hash, tx_idx: tx.out.index(output),
147
- hash160: Bitcoin::Script.new(output.pk_script).get_hash160 }
161
+ data = {tx_id: tx.hash, tx_idx: tx.out.index(output), hash160: output.parsed_script.get_hash160}
148
162
  txout = Bitcoin::Storage::Models::TxOut.new(self, data)
149
163
  [:value, :pk_script_length, :pk_script].each do |attr|
150
164
  txout.send("#{attr}=", output.send(attr))
@@ -156,5 +170,10 @@ module Bitcoin::Storage::Backends
156
170
  "DummyStore"
157
171
  end
158
172
 
173
+ def check_consistency(*args)
174
+ log.warn { "Dummy store doesn't support consistency check" }
175
+ end
176
+
177
+
159
178
  end
160
179
  end
@@ -95,13 +95,14 @@ module Bitcoin::Storage::Models
95
95
  # Transaction input retrieved from storage. (see Bitcoin::Protocol::TxIn
96
96
  class TxIn < Bitcoin::Protocol::TxIn
97
97
 
98
- attr_reader :store, :id, :tx_id, :tx_idx
98
+ attr_reader :store, :id, :tx_id, :tx_idx, :p2sh_type
99
99
 
100
100
  def initialize store, data
101
101
  @store = store
102
102
  @id = data[:id]
103
103
  @tx_id = data[:tx_id]
104
104
  @tx_idx = data[:tx_idx]
105
+ @p2sh_type = data[:p2sh_type]
105
106
  end
106
107
 
107
108
  # get the transaction this input is in
@@ -134,7 +135,7 @@ module Bitcoin::Storage::Models
134
135
  end
135
136
 
136
137
  def hash160
137
- script.get_hash160
138
+ parsed_script.get_hash160
138
139
  end
139
140
 
140
141
  # get the transaction this output is in
@@ -145,18 +146,16 @@ module Bitcoin::Storage::Models
145
146
  # get the next input that references this output
146
147
  def get_next_in
147
148
  @store.get_txin_for_txout(get_tx.hash, @tx_idx)
148
- rescue
149
- nil
150
149
  end
151
150
 
152
151
  # get all addresses this txout corresponds to (if possible)
153
152
  def get_address
154
- script.get_address
153
+ parsed_script.get_address
155
154
  end
156
155
 
157
156
  # get the single address this txout corresponds to (first for multisig tx)
158
157
  def get_addresses
159
- script.get_addresses
158
+ parsed_script.get_addresses
160
159
  end
161
160
 
162
161
  def get_namecoin_name
@@ -164,11 +163,7 @@ module Bitcoin::Storage::Models
164
163
  end
165
164
 
166
165
  def type
167
- script.type
168
- end
169
-
170
- def script
171
- @_script = Bitcoin::Script.new(@pk_script)
166
+ parsed_script.type
172
167
  end
173
168
 
174
169
  end
@@ -0,0 +1,14 @@
1
+ Sequel.migration do
2
+
3
+ up do
4
+
5
+ @log.info { "Running migration #{__FILE__}" }
6
+
7
+ if adapter_scheme == :postgres
8
+ execute "DROP VIEW unconfirmed" if self.views.include?(:unconfirmed)
9
+ execute "ALTER TABLE tx ALTER COLUMN hash TYPE bytea USING hash::bytea"
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,31 @@
1
+ Sequel.migration do
2
+
3
+ up do
4
+
5
+ @log.info { "Running migration #{__FILE__}" }
6
+
7
+ def process_block blk
8
+ print "\r#{blk.hash} - #{blk.depth}"
9
+ blk.tx.each do |tx|
10
+ self[:tx].where(hash: tx.hash.htb.blob).update(nhash: tx.nhash.htb.blob)
11
+ end
12
+ end
13
+
14
+ if @store.config[:index_nhash]
15
+ puts "Building normalized hash index..."
16
+
17
+ add_column :tx, :nhash, :bytea
18
+
19
+ if blk = @store.get_block_by_depth(0)
20
+ process_block(blk)
21
+ while blk = blk.get_next_block
22
+ process_block(blk)
23
+ end
24
+ end
25
+
26
+ add_index :tx, :nhash
27
+
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,16 @@
1
+ Sequel.migration do
2
+
3
+ up do
4
+
5
+ @log.info { "Running migration #{__FILE__}" }
6
+
7
+ # Naming seems to be different on different adapters and sequel's
8
+ # "drop_index(:txin, :prev_out)" doesn't seem to be handling it correctly
9
+ execute "DROP INDEX IF EXISTS txin_prev_out_idx;"
10
+ execute "DROP INDEX IF EXISTS txin_prev_out_index;"
11
+
12
+ add_index :txin, [:prev_out, :prev_out_index]
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,31 @@
1
+ # Add column txin.p2sh_type and index the type of the inner p2sh script of all inputs
2
+
3
+ Sequel.migration do
4
+
5
+ up do
6
+
7
+ @log.info { "Running migration #{__FILE__}" }
8
+
9
+ if @store.config[:index_p2sh_type]
10
+ puts "Building p2sh type index..."
11
+
12
+ add_column :txin, :p2sh_type, :int
13
+
14
+ self[:txout].where(type: 4).each do |txout|
15
+ tx = self[:tx][id: txout[:tx_id]]
16
+ next unless next_in = self[:txin][prev_out: tx[:hash].reverse, prev_out_index: txout[:tx_idx]]
17
+ script = Bitcoin::Script.new(next_in[:script_sig], txout[:pk_script])
18
+ if script.is_p2sh?
19
+ inner_script = Bitcoin::Script.new(script.inner_p2sh_script)
20
+ p2sh_type = @store.class::SCRIPT_TYPES.index(inner_script.type)
21
+ self[:txin].where(id: next_in[:id]).update(p2sh_type: p2sh_type)
22
+ end
23
+
24
+ end
25
+
26
+ add_index :txin, [:id, :p2sh_type]
27
+
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,56 @@
1
+ # Add column addr.type and correct the type for all p2sh addresses
2
+
3
+ Sequel.migration do
4
+
5
+ up do
6
+
7
+ @log.info { "Running migration #{__FILE__}" }
8
+
9
+ puts "Fixing address types for #{self[:txout].where(type: 4).count} p2sh addresses..."
10
+
11
+ add_column :addr, :type, :int, default: 0, null: false
12
+
13
+ i = 0
14
+ # iterate over all txouts with p2sh type
15
+ self[:txout].where(type: 4).each do |txout|
16
+ # find addr_txout mapping
17
+ addr_txout = self[:addr_txout][txout_id: txout[:id]]
18
+
19
+ # find currently linked address
20
+ addr = self[:addr][id: addr_txout[:addr_id]]
21
+
22
+ # skip if address type is already p2sh
23
+ next i+=1 if addr[:type] == 1
24
+
25
+ # if address has other txouts, that are not p2sh-type, we need a different one
26
+ if self[:addr_txout].where(addr_id: addr[:id])
27
+ .join(:txout, id: :txout_id).where("type != 4").any?
28
+
29
+ # if there is already a corrected address
30
+ if a = self[:addr][hash160: addr[:hash160], type: 1]
31
+ # use the existing corrected address
32
+ addr_id = a[:id]
33
+ else
34
+ # create new address with correct p2sh type
35
+ addr_id = self[:addr].insert(hash160: addr[:hash160], type: 1)
36
+ end
37
+
38
+ # change mapping to point to new address
39
+ self[:addr_txout].where(txout_id: txout[:id]).update(addr_id: addr_id)
40
+
41
+ # if address has only this txout
42
+ else
43
+ # change to correct type
44
+ self[:addr].where(id: addr[:id]).update(type: 1)
45
+ end
46
+
47
+ print "\r#{i}"; i+=1
48
+
49
+ end
50
+ puts
51
+
52
+ add_index :addr, [:hash160, :type]
53
+
54
+ end
55
+
56
+ end
@@ -7,12 +7,21 @@ module Bitcoin::Storage::Backends
7
7
 
8
8
  # Storage backend using Sequel to connect to arbitrary SQL databases.
9
9
  # Inherits from StoreBase and implements its interface.
10
- class SequelStore < StoreBase
10
+ class SequelStore < SequelStoreBase
11
11
 
12
12
  # sequel database connection
13
13
  attr_accessor :db
14
14
 
15
- DEFAULT_CONFIG = { mode: :full, cache_head: false }
15
+ DEFAULT_CONFIG = {
16
+ # TODO
17
+ mode: :full,
18
+
19
+ # cache head block. only the instance that is updating the head should do this.
20
+ cache_head: false,
21
+
22
+ # store an index of tx.nhash values
23
+ index_nhash: false
24
+ }
16
25
 
17
26
  # create sequel store with given +config+
18
27
  def initialize config, *args
@@ -56,33 +65,38 @@ module Bitcoin::Storage::Backends
56
65
  blk_tx, new_tx, addrs, names = [], [], [], []
57
66
 
58
67
  # store tx
68
+ existing_tx = Hash[*@db[:tx].filter(hash: blk.tx.map {|tx| tx.hash.htb.blob }).map { |tx| [tx[:hash].hth, tx[:id]] }.flatten]
59
69
  blk.tx.each.with_index do |tx, idx|
60
- existing = @db[:tx][hash: tx.hash.htb.blob]
61
- existing ? blk_tx[idx] = existing[:id] : new_tx << [tx, idx]
70
+ existing = existing_tx[tx.hash]
71
+ existing ? blk_tx[idx] = existing : new_tx << [tx, idx]
62
72
  end
63
- new_tx_ids = @db[:tx].insert_multiple(new_tx.map {|tx, _| tx_data(tx) })
73
+
74
+ new_tx_ids = fast_insert(:tx, new_tx.map {|tx, _| tx_data(tx) }, return_ids: true)
64
75
  new_tx_ids.each.with_index {|tx_id, idx| blk_tx[new_tx[idx][1]] = tx_id }
65
76
 
66
- @db[:blk_tx].insert_multiple(blk_tx.map.with_index {|id, idx|
67
- { blk_id: block_id, tx_id: id, idx: idx } })
77
+ fast_insert(:blk_tx, blk_tx.map.with_index {|id, idx| { blk_id: block_id, tx_id: id, idx: idx } })
68
78
 
69
79
  # store txins
70
- txin_ids = @db[:txin].insert_multiple(new_tx.map.with_index {|tx, tx_idx|
80
+ fast_insert(:txin, new_tx.map.with_index {|tx, tx_idx|
71
81
  tx, _ = *tx
72
82
  tx.in.map.with_index {|txin, txin_idx|
73
- txin_data(new_tx_ids[tx_idx], txin, txin_idx) } }.flatten)
83
+ p2sh_type = nil
84
+ if @config[:index_p2sh_type] && !txin.coinbase? && (script = tx.scripts[txin_idx]) && script.is_p2sh?
85
+ p2sh_type = Bitcoin::Script.new(script.inner_p2sh_script).type
86
+ end
87
+ txin_data(new_tx_ids[tx_idx], txin, txin_idx, p2sh_type) } }.flatten)
74
88
 
75
89
  # store txouts
76
90
  txout_i = 0
77
- txout_ids = @db[:txout].insert_multiple(new_tx.map.with_index {|tx, tx_idx|
91
+ txout_ids = fast_insert(:txout, new_tx.map.with_index {|tx, tx_idx|
78
92
  tx, _ = *tx
79
93
  tx.out.map.with_index {|txout, txout_idx|
80
- script_type, a, n = *parse_script(txout, txout_i, tx.hash)
94
+ script_type, a, n = *parse_script(txout, txout_i, tx.hash, txout_idx)
81
95
  addrs += a; names += n; txout_i += 1
82
- txout_data(new_tx_ids[tx_idx], txout, txout_idx, script_type) } }.flatten)
96
+ txout_data(new_tx_ids[tx_idx], txout, txout_idx, script_type) } }.flatten, return_ids: true)
83
97
 
84
98
  # store addrs
85
- persist_addrs addrs.map {|i, h| [txout_ids[i], h]}
99
+ persist_addrs addrs.map {|i, addr| [txout_ids[i], addr]}
86
100
  names.each {|i, script| store_name(script, txout_ids[i]) }
87
101
  end
88
102
  @head = wrap_block(attrs.merge(id: block_id)) if chain == MAIN
@@ -101,65 +115,70 @@ module Bitcoin::Storage::Backends
101
115
  def reorg new_side, new_main
102
116
  @db.transaction do
103
117
  @db[:blk].where(hash: new_side.map {|h| h.htb.blob }).update(chain: SIDE)
104
- new_main.each {|b| get_block(b).validator(self).validate(raise_errors: true) } unless @config[:skip_validation]
105
- @db[:blk].where(hash: new_main.map {|h| h.htb.blob }).update(chain: MAIN)
106
- end
107
- end
108
-
109
- # parse script and collect address/txout mappings to index
110
- def parse_script txout, i, tx_hash = ""
111
- addrs, names = [], []
112
-
113
- script = Bitcoin::Script.new(txout.pk_script) rescue nil
114
- if script
115
- if script.is_hash160? || script.is_pubkey?
116
- addrs << [i, script.get_hash160]
117
- elsif script.is_multisig?
118
- script.get_multisig_pubkeys.map do |pubkey|
119
- addrs << [i, Bitcoin.hash160(pubkey.unpack("H*")[0])]
118
+ new_main.each do |block_hash|
119
+ unless @config[:skip_validation]
120
+ get_block(block_hash).validator(self).validate(raise_errors: true)
120
121
  end
121
- elsif Bitcoin.namecoin? && script.is_namecoin?
122
- addrs << [i, script.get_hash160]
123
- names << [i, script]
124
- else
125
- log.info { "Unknown script type in #{tx_hash}:#{i}" }
126
- log.debug { script.to_string }
122
+ @db[:blk].where(hash: block_hash.htb.blob).update(chain: MAIN)
127
123
  end
128
- script_type = SCRIPT_TYPES.index(script.type)
129
- else
130
- log.error { "Error parsing script #{tx_hash}:#{i}" }
131
- script_type = SCRIPT_TYPES.index(:unknown)
132
124
  end
133
- [script_type, addrs, names]
134
125
  end
135
126
 
136
127
  # bulk-store addresses and txout mappings
137
128
  def persist_addrs addrs
138
129
  addr_txouts, new_addrs = [], []
139
- addrs.group_by {|_, a| a }.each do |hash160, txouts|
140
- if existing = @db[:addr][:hash160 => hash160]
141
- txouts.each {|id, _| addr_txouts << [existing[:id], id] }
130
+
131
+ # find addresses that are already there
132
+ existing_addr = {}
133
+ addrs.each do |i, addr|
134
+ hash160 = Bitcoin.hash160_from_address(addr)
135
+ type = Bitcoin.address_type(addr)
136
+ if existing = @db[:addr][hash160: hash160, type: ADDRESS_TYPES.index(type)]
137
+ existing_addr[[hash160, type]] = existing[:id]
138
+ end
139
+ end
140
+
141
+ # iterate over all txouts, grouped by hash160
142
+ addrs.group_by {|_, a| a }.each do |addr, txouts|
143
+ hash160 = Bitcoin.hash160_from_address(addr)
144
+ type = Bitcoin.address_type(addr)
145
+
146
+ if existing_id = existing_addr[[hash160, type]]
147
+ # link each txout to existing address
148
+ txouts.each {|id, _| addr_txouts << [existing_id, id] }
142
149
  else
143
- new_addrs << [hash160, txouts.map {|id, _| id }]
150
+ # collect new address/txout mapping
151
+ new_addrs << [[hash160, type], txouts.map {|id, _| id }]
144
152
  end
145
153
  end
146
- new_addr_ids = @db[:addr].insert_multiple(new_addrs.map {|hash160, txout_id|
147
- { hash160: hash160 } })
154
+
155
+ # insert all new addresses
156
+ new_addr_ids = fast_insert(:addr, new_addrs.map {|hash160_and_type, txout_id|
157
+ hash160, type = *hash160_and_type
158
+ { hash160: hash160, type: ADDRESS_TYPES.index(type) }
159
+ }, return_ids: true)
160
+
161
+
162
+ # link each new txout to the new addresses
148
163
  new_addr_ids.each.with_index do |addr_id, idx|
149
164
  new_addrs[idx][1].each do |txout_id|
150
165
  addr_txouts << [addr_id, txout_id]
151
166
  end
152
167
  end
153
- @db[:addr_txout].insert_multiple(addr_txouts.map {|addr_id, txout_id|
154
- { addr_id: addr_id, txout_id: txout_id }})
168
+
169
+ # insert addr/txout links
170
+ fast_insert(:addr_txout, addr_txouts.map {|addr_id, txout_id| { addr_id: addr_id, txout_id: txout_id }})
155
171
  end
156
172
 
157
173
  # prepare transaction data for storage
158
174
  def tx_data tx
159
- { hash: tx.hash.htb.blob,
175
+ data = {
176
+ hash: tx.hash.htb.blob,
160
177
  version: tx.ver, lock_time: tx.lock_time,
161
178
  coinbase: tx.in.size == 1 && tx.in[0].coinbase?,
162
179
  tx_size: tx.payload.bytesize }
180
+ data[:nhash] = tx.nhash.htb.blob if @config[:index_nhash]
181
+ data
163
182
  end
164
183
 
165
184
  # store transaction +tx+
@@ -177,17 +196,21 @@ module Bitcoin::Storage::Backends
177
196
  end
178
197
 
179
198
  # prepare txin data for storage
180
- def txin_data tx_id, txin, idx
181
- { tx_id: tx_id, tx_idx: idx,
199
+ def txin_data tx_id, txin, idx, p2sh_type = nil
200
+ data = {
201
+ tx_id: tx_id, tx_idx: idx,
182
202
  script_sig: txin.script_sig.blob,
183
203
  prev_out: txin.prev_out.blob,
184
204
  prev_out_index: txin.prev_out_index,
185
- sequence: txin.sequence.unpack("V")[0] }
205
+ sequence: txin.sequence.unpack("V")[0],
206
+ }
207
+ data[:p2sh_type] = SCRIPT_TYPES.index(p2sh_type) if @config[:index_p2sh_type]
208
+ data
186
209
  end
187
210
 
188
211
  # store input +txin+
189
- def store_txin(tx_id, txin, idx)
190
- @db[:txin].insert(txin_data(tx_id, txin, idx))
212
+ def store_txin(tx_id, txin, idx, p2sh_type = nil)
213
+ @db[:txin].insert(txin_data(tx_id, txin, idx, p2sh_type))
191
214
  end
192
215
 
193
216
  # prepare txout data for storage
@@ -199,7 +222,7 @@ module Bitcoin::Storage::Backends
199
222
 
200
223
  # store output +txout+
201
224
  def store_txout(tx_id, txout, idx, tx_hash = "")
202
- script_type, addrs, names = *parse_script(txout, idx, tx_hash)
225
+ script_type, addrs, names = *parse_script(txout, idx, tx_hash, idx)
203
226
  txout_id = @db[:txout].insert(txout_data(tx_id, txout, idx, script_type))
204
227
  persist_addrs addrs.map {|i, h| [txout_id, h] }
205
228
  names.each {|i, script| store_name(script, txout_id) }
@@ -218,9 +241,9 @@ module Bitcoin::Storage::Backends
218
241
  end
219
242
  end
220
243
 
221
- # check if block +blk_hash+ exists
244
+ # check if block +blk_hash+ exists in the main chain
222
245
  def has_block(blk_hash)
223
- !!@db[:blk].where(:hash => blk_hash.htb.blob).get(1)
246
+ !!@db[:blk].where(:hash => blk_hash.htb.blob, :chain => 0).get(1)
224
247
  end
225
248
 
226
249
  # check if transaction +tx_hash+ exists
@@ -277,11 +300,31 @@ module Bitcoin::Storage::Backends
277
300
  wrap_block(@db[:blk][:id => block_id])
278
301
  end
279
302
 
303
+ # get block id in the main chain by given +tx_id+
304
+ def get_block_id_for_tx_id(tx_id)
305
+ @db[:blk_tx].join(:blk, id: :blk_id)
306
+ .where(tx_id: tx_id, chain: MAIN).first[:blk_id] rescue nil
307
+ end
308
+
280
309
  # get transaction for given +tx_hash+
281
310
  def get_tx(tx_hash)
282
311
  wrap_tx(@db[:tx][:hash => tx_hash.htb.blob])
283
312
  end
284
313
 
314
+ # get array of txes with given +tx_hashes+
315
+ def get_txs(tx_hashes)
316
+ txs = db[:tx].filter(hash: tx_hashes.map{|h| h.htb.blob})
317
+ txs_ids = txs.map {|tx| tx[:id]}
318
+ return [] if txs_ids.empty?
319
+
320
+ # we fetch all needed block ids, inputs and outputs to avoid doing number of queries propertional to number of transactions
321
+ block_ids = Hash[*db[:blk_tx].join(:blk, id: :blk_id).filter(tx_id: txs_ids, chain: 0).map {|b| [b[:tx_id], b[:blk_id]] }.flatten]
322
+ inputs = db[:txin].filter(:tx_id => txs_ids).order(:tx_idx).map.group_by{ |txin| txin[:tx_id] }
323
+ outputs = db[:txout].filter(:tx_id => txs_ids).order(:tx_idx).map.group_by{ |txout| txout[:tx_id] }
324
+
325
+ txs.map {|tx| wrap_tx(tx, block_ids[tx[:id]], inputs: inputs[tx[:id]], outputs: outputs[tx[:id]]) }
326
+ end
327
+
285
328
  # get transaction by given +tx_id+
286
329
  def get_tx_by_id(tx_id)
287
330
  wrap_tx(@db[:tx][:id => tx_id])
@@ -294,6 +337,11 @@ module Bitcoin::Storage::Backends
294
337
  wrap_txin(@db[:txin][:prev_out => tx_hash, :prev_out_index => txout_idx])
295
338
  end
296
339
 
340
+ # optimized version of Storage#get_txins_for_txouts
341
+ def get_txins_for_txouts(txouts)
342
+ @db[:txin].filter([:prev_out, :prev_out_index] => txouts.map{|tx_hash, tx_idx| [tx_hash.htb_reverse.blob, tx_idx]}).map{|i| wrap_txin(i)}
343
+ end
344
+
297
345
  def get_txout_by_id(txout_id)
298
346
  wrap_txout(@db[:txout][:id => txout_id])
299
347
  end
@@ -312,14 +360,14 @@ module Bitcoin::Storage::Backends
312
360
  end
313
361
 
314
362
  # get all Models::TxOut matching given +hash160+
315
- def get_txouts_for_hash160(hash160, unconfirmed = false)
316
- addr = @db[:addr][:hash160 => hash160]
363
+ def get_txouts_for_hash160(hash160, type = :hash160, unconfirmed = false)
364
+ addr = @db[:addr][hash160: hash160, type: ADDRESS_TYPES.index(type)]
317
365
  return [] unless addr
318
- txouts = @db[:addr_txout].where(:addr_id => addr[:id])
319
- .map{|t| @db[:txout][:id => t[:txout_id]] }
366
+ txouts = @db[:addr_txout].where(addr_id: addr[:id])
367
+ .map{|t| @db[:txout][id: t[:txout_id]] }
320
368
  .map{|o| wrap_txout(o) }
321
369
  unless unconfirmed
322
- txouts.select!{|o| @db[:blk][:id => o.get_tx.blk_id][:chain] == MAIN rescue false }
370
+ txouts.select!{|o| @db[:blk][id: o.get_tx.blk_id][:chain] == MAIN rescue false }
323
371
  end
324
372
  txouts
325
373
  end
@@ -358,15 +406,20 @@ module Bitcoin::Storage::Backends
358
406
 
359
407
  blk.aux_pow = Bitcoin::P::AuxPow.new(block[:aux_pow]) if block[:aux_pow]
360
408
 
361
- db[:blk_tx].filter(blk_id: block[:id]).join(:tx, id: :tx_id)
362
- .order(:idx).each {|tx| blk.tx << wrap_tx(tx, block[:id]) }
409
+ blk_tx = db[:blk_tx].filter(blk_id: block[:id]).join(:tx, id: :tx_id).order(:idx)
410
+
411
+ # fetch inputs and outputs for all transactions in the block to avoid additional queries for each transaction
412
+ inputs = db[:txin].filter(:tx_id => blk_tx.map{ |tx| tx[:id] }).order(:tx_idx).map.group_by{ |txin| txin[:tx_id] }
413
+ outputs = db[:txout].filter(:tx_id => blk_tx.map{ |tx| tx[:id] }).order(:tx_idx).map.group_by{ |txout| txout[:tx_id] }
363
414
 
364
- blk.recalc_block_hash
415
+ blk.tx = blk_tx.map { |tx| wrap_tx(tx, block[:id], inputs: inputs[tx[:id]], outputs: outputs[tx[:id]]) }
416
+
417
+ blk.hash = block[:hash].hth
365
418
  blk
366
419
  end
367
420
 
368
421
  # wrap given +transaction+ into Models::Transaction
369
- def wrap_tx(transaction, block_id = nil)
422
+ def wrap_tx(transaction, block_id = nil, prefetched = {})
370
423
  return nil unless transaction
371
424
 
372
425
  block_id ||= @db[:blk_tx].join(:blk, id: :blk_id)
@@ -375,21 +428,22 @@ module Bitcoin::Storage::Backends
375
428
  data = {id: transaction[:id], blk_id: block_id, size: transaction[:tx_size], idx: transaction[:idx]}
376
429
  tx = Bitcoin::Storage::Models::Tx.new(self, data)
377
430
 
378
- inputs = db[:txin].filter(:tx_id => transaction[:id]).order(:tx_idx)
431
+ inputs = prefetched[:inputs] || db[:txin].filter(:tx_id => transaction[:id]).order(:tx_idx)
379
432
  inputs.each { |i| tx.add_in(wrap_txin(i)) }
380
433
 
381
- outputs = db[:txout].filter(:tx_id => transaction[:id]).order(:tx_idx)
434
+ outputs = prefetched[:outputs] || db[:txout].filter(:tx_id => transaction[:id]).order(:tx_idx)
382
435
  outputs.each { |o| tx.add_out(wrap_txout(o)) }
383
436
  tx.ver = transaction[:version]
384
437
  tx.lock_time = transaction[:lock_time]
385
- tx.hash = tx.hash_from_payload(tx.to_payload)
438
+ tx.hash = transaction[:hash].hth
386
439
  tx
387
440
  end
388
441
 
389
442
  # wrap given +input+ into Models::TxIn
390
443
  def wrap_txin(input)
391
444
  return nil unless input
392
- data = {:id => input[:id], :tx_id => input[:tx_id], :tx_idx => input[:tx_idx]}
445
+ data = { :id => input[:id], :tx_id => input[:tx_id], :tx_idx => input[:tx_idx],
446
+ :p2sh_type => input[:p2sh_type] ? SCRIPT_TYPES[input[:p2sh_type]] : nil }
393
447
  txin = Bitcoin::Storage::Models::TxIn.new(self, data)
394
448
  txin.prev_out = input[:prev_out]
395
449
  txin.prev_out_index = input[:prev_out_index]
@@ -448,6 +502,50 @@ module Bitcoin::Storage::Backends
448
502
  # end
449
503
  end
450
504
 
505
+ protected
506
+
507
+ # Abstraction for doing many quick inserts.
508
+ #
509
+ # * +table+ - db table name
510
+ # * +data+ - a table of hashes with the same keys
511
+ # * +opts+
512
+ # ** return_ids - if true table of inserted rows ids will be returned
513
+ def fast_insert(table, data, opts={})
514
+ return [] if data.empty?
515
+ # For postgres we are using COPY which is much faster than separate INSERTs
516
+ if @db.adapter_scheme == :postgres
517
+
518
+ columns = data.first.keys
519
+ if opts[:return_ids]
520
+ ids = db.transaction do
521
+ # COPY does not return ids, so we set ids manually based on current sequence value
522
+ # We lock the table to avoid inserts that could happen in the middle of COPY
523
+ db.execute("LOCK TABLE #{table} IN SHARE UPDATE EXCLUSIVE MODE")
524
+ first_id = db.fetch("SELECT nextval('#{table}_id_seq') AS id").first[:id]
525
+
526
+ # Blobs need to be represented in the hex form (yes, we do hth on them earlier, could be improved
527
+ # \\x is the format of bytea as hex encoding in postgres
528
+ csv = data.map.with_index{|x,i| [first_id + i, columns.map{|c| x[c].kind_of?(Sequel::SQL::Blob) ? "\\x#{x[c].hth}" : x[c]}].join(',')}.join("\n")
529
+ db.copy_into(table, columns: [:id] + columns, format: :csv, data: csv)
530
+ last_id = first_id + data.size - 1
531
+
532
+ # Set sequence value to max id, last arg true means it will be incremented before next value
533
+ db.execute("SELECT setval('#{table}_id_seq', #{last_id}, true)")
534
+ (first_id..last_id).to_a # returned ids
535
+ end
536
+ else
537
+ csv = data.map{|x| columns.map{|c| x[c].kind_of?(Sequel::SQL::Blob) ? "\\x#{x[c].hth}" : x[c]}.join(',')}.join("\n")
538
+ @db.copy_into(table, format: :csv, columns: columns, data: csv)
539
+ end
540
+
541
+ else
542
+
543
+ # Life is simple when your are not optimizing ;)
544
+ @db[table].insert_multiple(data)
545
+
546
+ end
547
+ end
548
+
451
549
  end
452
550
 
453
551
  end