bitcoin-ruby 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +4 -1
  2. data/Gemfile +21 -0
  3. data/README.rdoc +85 -25
  4. data/Rakefile +7 -3
  5. data/bin/bitcoin_node +39 -42
  6. data/bin/bitcoin_shell +1 -0
  7. data/bin/bitcoin_wallet +129 -53
  8. data/bitcoin-ruby.gemspec +4 -7
  9. data/concept-examples/blockchain-pow.rb +1 -1
  10. data/doc/CONFIG.rdoc +5 -5
  11. data/doc/EXAMPLES.rdoc +9 -5
  12. data/doc/NAMECOIN.rdoc +34 -0
  13. data/doc/NODE.rdoc +147 -10
  14. data/examples/balance.rb +10 -4
  15. data/examples/bbe_verify_tx.rb +7 -2
  16. data/examples/forwarder.rb +73 -0
  17. data/examples/generate_tx.rb +34 -0
  18. data/examples/simple_network_monitor_and_util.rb +187 -0
  19. data/examples/verify_tx.rb +1 -1
  20. data/lib/bitcoin.rb +308 -18
  21. data/lib/bitcoin/builder.rb +62 -36
  22. data/lib/bitcoin/config.rb +2 -0
  23. data/lib/bitcoin/connection.rb +11 -8
  24. data/lib/bitcoin/electrum/mnemonic.rb +162 -0
  25. data/lib/bitcoin/ffi/openssl.rb +187 -21
  26. data/lib/bitcoin/gui/addr_view.rb +2 -0
  27. data/lib/bitcoin/gui/conn_view.rb +2 -0
  28. data/lib/bitcoin/gui/connection.rb +2 -0
  29. data/lib/bitcoin/gui/em_gtk.rb +2 -0
  30. data/lib/bitcoin/gui/gui.rb +2 -0
  31. data/lib/bitcoin/gui/helpers.rb +2 -0
  32. data/lib/bitcoin/gui/tree_view.rb +2 -0
  33. data/lib/bitcoin/gui/tx_view.rb +2 -0
  34. data/lib/bitcoin/key.rb +77 -11
  35. data/lib/bitcoin/litecoin.rb +81 -0
  36. data/lib/bitcoin/logger.rb +20 -1
  37. data/lib/bitcoin/namecoin.rb +279 -0
  38. data/lib/bitcoin/network/command_client.rb +7 -6
  39. data/lib/bitcoin/network/command_handler.rb +229 -43
  40. data/lib/bitcoin/network/connection_handler.rb +182 -70
  41. data/lib/bitcoin/network/node.rb +231 -106
  42. data/lib/bitcoin/protocol.rb +44 -23
  43. data/lib/bitcoin/protocol/address.rb +5 -3
  44. data/lib/bitcoin/protocol/alert.rb +3 -4
  45. data/lib/bitcoin/protocol/aux_pow.rb +123 -0
  46. data/lib/bitcoin/protocol/block.rb +98 -18
  47. data/lib/bitcoin/protocol/handler.rb +6 -5
  48. data/lib/bitcoin/protocol/parser.rb +44 -19
  49. data/lib/bitcoin/protocol/tx.rb +105 -52
  50. data/lib/bitcoin/protocol/txin.rb +39 -19
  51. data/lib/bitcoin/protocol/txout.rb +28 -13
  52. data/lib/bitcoin/protocol/version.rb +16 -7
  53. data/lib/bitcoin/script.rb +579 -122
  54. data/lib/bitcoin/storage/{dummy.rb → dummy/dummy_store.rb} +8 -14
  55. data/lib/bitcoin/storage/models.rb +20 -7
  56. data/lib/bitcoin/storage/{sequel_store/sequel_migrations.rb → sequel/migrations.rb} +22 -7
  57. data/lib/bitcoin/storage/sequel/migrations/001_base_schema.rb +52 -0
  58. data/lib/bitcoin/storage/sequel/migrations/002_tx.rb +50 -0
  59. data/lib/bitcoin/storage/sequel/migrations/003_change_txin_script_sig_to_blob.rb +18 -0
  60. data/lib/bitcoin/storage/sequel/sequel_store.rb +436 -0
  61. data/lib/bitcoin/storage/storage.rb +233 -28
  62. data/lib/bitcoin/storage/utxo/migrations/001_base_schema.rb +52 -0
  63. data/lib/bitcoin/storage/utxo/migrations/002_utxo.rb +18 -0
  64. data/lib/bitcoin/storage/utxo/utxo_store.rb +361 -0
  65. data/lib/bitcoin/validation.rb +369 -0
  66. data/lib/bitcoin/version.rb +1 -1
  67. data/lib/bitcoin/wallet/coinselector.rb +3 -0
  68. data/lib/bitcoin/wallet/keygenerator.rb +3 -1
  69. data/lib/bitcoin/wallet/keystore.rb +6 -2
  70. data/lib/bitcoin/wallet/txdp.rb +6 -4
  71. data/lib/bitcoin/wallet/wallet.rb +54 -16
  72. data/spec/bitcoin/bitcoin_spec.rb +48 -3
  73. data/spec/bitcoin/builder_spec.rb +40 -17
  74. data/spec/bitcoin/fixtures/000000000000056b1a3d84a1e2b33cde8915a4b61c0cae14fca6d3e1490b4f98.json +3697 -0
  75. data/spec/bitcoin/fixtures/03d7e1fa4d5fefa169431f24f7798552861b255cd55d377066fedcd088fb0e99.json +23 -0
  76. data/spec/bitcoin/fixtures/0961c660358478829505e16a1f028757e54b5bbf9758341a7546573738f31429.json +24 -0
  77. data/spec/bitcoin/fixtures/0f24294a1d23efbb49c1765cf443fba7930702752aba6d765870082fe4f13cae.json +37 -0
  78. data/spec/bitcoin/fixtures/315ac7d4c26d69668129cc352851d9389b4a6868f1509c6c8b66bead11e2619f.json +31 -0
  79. data/spec/bitcoin/fixtures/35e2001b428891fefa0bfb73167c7360669d3cbd7b3aa78e7cad125ddfc51131.json +27 -0
  80. data/spec/bitcoin/fixtures/3a17dace09ffb919ed627a93f1873220f4c975c1248558b18d16bce25d38c4b7.json +72 -0
  81. data/spec/bitcoin/fixtures/3e58b7eed0fdb599019af08578effea25c8666bbe8e200845453cacce6314477.json +27 -0
  82. data/spec/bitcoin/fixtures/514c46f0b61714092f15c8dfcb576c9f79b3f959989b98de3944b19d98832b58.json +24 -0
  83. data/spec/bitcoin/fixtures/51bf528ecf3c161e7c021224197dbe84f9a8564212f6207baa014c01a1668e1e.json +30 -0
  84. data/spec/bitcoin/fixtures/69216b8aaa35b76d6613e5f527f4858640d986e1046238583bdad79b35e938dc.json +28 -0
  85. data/spec/bitcoin/fixtures/7208e5edf525f04e705fb3390194e316205b8f995c8c9fcd8c6093abe04fa27d.json +27 -0
  86. data/spec/bitcoin/fixtures/761d8c5210fdfd505f6dff38f740ae3728eb93d7d0971fb433f685d40a4c04f6.json +27 -0
  87. data/spec/bitcoin/fixtures/aea682d68a3ea5e3583e088dcbd699a5d44d4b083f02ad0aaf2598fe1fa4dfd4.json +27 -0
  88. data/spec/bitcoin/fixtures/bd1715f1abfdc62bea3f605bdb461b3ba1f2cca6ec0d73a18a548b7717ca8531.json +34 -0
  89. data/spec/bitcoin/fixtures/block-testnet-0000000000ac85bb2530a05a4214a387e6be02b22d3348abc5e7a5d9c4ce8dab.bin +0 -0
  90. data/spec/bitcoin/fixtures/cd874fa8cb0e2ec2d385735d5e1fd482c4fe648533efb4c50ee53bda58e15ae2.json +24 -0
  91. data/spec/bitcoin/fixtures/ce5fad9b4ef094d8f4937b0707edaf0a6e6ceeaf67d5edbfd51f660eac8f398b.json +41 -0
  92. data/spec/bitcoin/fixtures/f003f0c1193019db2497a675fd05d9f2edddf9b67c59e677c48d3dbd4ed5f00b.json +23 -0
  93. data/spec/bitcoin/fixtures/freicoin-block-000000005d231b285e63af83edae2d8f5e50e70d396468643092b9239fd3be3c.bin +0 -0
  94. data/spec/bitcoin/fixtures/freicoin-block-000000005d231b285e63af83edae2d8f5e50e70d396468643092b9239fd3be3c.json +43 -0
  95. data/spec/bitcoin/fixtures/freicoin-genesis-block-000000005b1e3d23ecfd2dd4a6e1a35238aa0392c0a8528c40df52376d7efe2c.bin +0 -0
  96. data/spec/bitcoin/fixtures/freicoin-genesis-block-000000005b1e3d23ecfd2dd4a6e1a35238aa0392c0a8528c40df52376d7efe2c.json +67 -0
  97. data/spec/bitcoin/fixtures/litecoin-block-80ca095ed10b02e53d769eb6eaf92cd04e9e0759e5be4a8477b42911ba49c78f.bin +0 -0
  98. data/spec/bitcoin/fixtures/litecoin-block-80ca095ed10b02e53d769eb6eaf92cd04e9e0759e5be4a8477b42911ba49c78f.json +39 -0
  99. data/spec/bitcoin/fixtures/litecoin-genesis-block-12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2.bin +0 -0
  100. data/spec/bitcoin/fixtures/litecoin-genesis-block-12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2.json +39 -0
  101. data/spec/bitcoin/fixtures/rawblock-auxpow.bin +0 -0
  102. data/spec/bitcoin/fixtures/tx-313897799b1e37e9ecae15010e56156dddde4e683c96b0e713af95272c38aee0.json +30 -0
  103. data/spec/bitcoin/fixtures/tx-3da75972766f0ad13319b0b461fd16823a731e44f6e9de4eb3c52d6a6fb6c8ae.json +23 -0
  104. data/spec/bitcoin/fixtures/tx-44b833074e671120ba33106877b49e86ece510824b9af477a3853972bcd8d06a.json +30 -0
  105. data/spec/bitcoin/fixtures/tx-d3d77d63709e47d9ef58f0b557800115a6b676c6a423012fbb96f45d8fcef830.json +28 -0
  106. data/spec/bitcoin/key_spec.rb +128 -3
  107. data/spec/bitcoin/namecoin_spec.rb +182 -0
  108. data/spec/bitcoin/network_spec.rb +5 -3
  109. data/spec/bitcoin/node/command_api_spec.rb +376 -0
  110. data/spec/bitcoin/protocol/addr_spec.rb +2 -0
  111. data/spec/bitcoin/protocol/alert_spec.rb +2 -0
  112. data/spec/bitcoin/protocol/aux_pow_spec.rb +44 -0
  113. data/spec/bitcoin/protocol/block_spec.rb +134 -39
  114. data/spec/bitcoin/protocol/getblocks_spec.rb +32 -0
  115. data/spec/bitcoin/protocol/inv_spec.rb +10 -8
  116. data/spec/bitcoin/protocol/notfound_spec.rb +31 -0
  117. data/spec/bitcoin/protocol/ping_spec.rb +2 -0
  118. data/spec/bitcoin/protocol/tx_spec.rb +83 -17
  119. data/spec/bitcoin/protocol/version_spec.rb +7 -5
  120. data/spec/bitcoin/script/opcodes_spec.rb +412 -133
  121. data/spec/bitcoin/script/script_spec.rb +112 -13
  122. data/spec/bitcoin/spec_helper.rb +68 -0
  123. data/spec/bitcoin/storage/reorg_spec.rb +199 -0
  124. data/spec/bitcoin/storage/storage_spec.rb +337 -0
  125. data/spec/bitcoin/storage/validation_spec.rb +261 -0
  126. data/spec/bitcoin/wallet/coinselector_spec.rb +10 -7
  127. data/spec/bitcoin/wallet/keygenerator_spec.rb +2 -0
  128. data/spec/bitcoin/wallet/keystore_spec.rb +2 -0
  129. data/spec/bitcoin/wallet/txdp_spec.rb +2 -0
  130. data/spec/bitcoin/wallet/wallet_spec.rb +91 -58
  131. metadata +105 -51
  132. data/lib/bitcoin/storage/sequel.rb +0 -335
  133. data/spec/bitcoin/fixtures/0d0affb5964abe804ffe85e53f1dbb9f29e406aa3046e2db04fba240e63c7fdd.json +0 -27
  134. data/spec/bitcoin/fixtures/477fff140b363ec2cc51f3a65c0c58eda38f4d41f04a295bbd62babf25e4c590.json +0 -27
  135. data/spec/bitcoin/reorg_spec.rb +0 -129
  136. data/spec/bitcoin/storage_spec.rb +0 -229
@@ -1,3 +1,5 @@
1
+ # encoding: ascii-8bit
2
+
1
3
  # The storage implementation supports different backends, which inherit from
2
4
  # Storage::StoreBase and implement the same interface.
3
5
  # Each backend returns Storage::Models objects to easily access helper methods and metadata.
@@ -11,7 +13,7 @@ module Bitcoin::Storage
11
13
  @log = Bitcoin::Logger.create(:storage)
12
14
  def self.log; @log; end
13
15
 
14
- BACKENDS = [:dummy, :sequel]
16
+ BACKENDS = [:dummy, :sequel, :utxo]
15
17
  BACKENDS.each do |name|
16
18
  module_eval <<-EOS
17
19
  def self.#{name} config, *args
@@ -22,7 +24,7 @@ module Bitcoin::Storage
22
24
 
23
25
  module Backends
24
26
 
25
- BACKENDS.each {|b| autoload("#{b.to_s.capitalize}Store", "bitcoin/storage/#{b}") }
27
+ BACKENDS.each {|b| autoload("#{b.to_s.capitalize}Store", "bitcoin/storage/#{b}/#{b}_store.rb") }
26
28
 
27
29
  # Base class for storage backends.
28
30
  # Every backend must overwrite the "Not implemented" methods
@@ -40,12 +42,102 @@ module Bitcoin::Storage
40
42
  # orphan branch (not connected to main branch / genesis block)
41
43
  ORPHAN = 2
42
44
 
43
- attr_reader :log
45
+ # possible script types
46
+ SCRIPT_TYPES = [:unknown, :pubkey, :hash160, :multisig, :p2sh]
47
+ if Bitcoin.namecoin?
48
+ [:name_new, :name_firstupdate, :name_update].each {|n| SCRIPT_TYPES << n }
49
+ end
50
+
51
+ DEFAULT_CONFIG = {
52
+ sqlite_pragmas: {
53
+ # journal_mode pragma
54
+ journal_mode: false,
55
+ # synchronous pragma
56
+ synchronous: false,
57
+ # cache_size pragma
58
+ # positive specifies number of cache pages to use,
59
+ # negative specifies cache size in kilobytes.
60
+ cache_size: -200_000,
61
+ }
62
+ }
63
+
64
+ SEQUEL_ADAPTERS = { :sqlite => "sqlite3", :postgres => "pg", :mysql => "mysql" }
65
+
66
+ attr_reader :log, :config
67
+
68
+ attr_accessor :config
44
69
 
45
70
  def initialize(config = {}, getblocks_callback = nil)
46
- @config = config
47
- @getblocks_callback = getblocks_callback
71
+ base = self.class.ancestors.select {|a| a.name =~ /StoreBase$/ }[0]::DEFAULT_CONFIG
72
+ @config = base.merge(self.class::DEFAULT_CONFIG).merge(config)
48
73
  @log = config[:log] || Bitcoin::Storage.log
74
+ @log.level = @config[:log_level] if @config[:log_level]
75
+ init_sequel_store
76
+ @getblocks_callback = getblocks_callback
77
+ @checkpoints = Bitcoin.network[:checkpoints] || {}
78
+ @watched_addrs = []
79
+ end
80
+
81
+ def init_sequel_store
82
+ return unless (self.is_a?(SequelStore) || self.is_a?(UtxoStore)) && @config[:db]
83
+ @config[:db].sub!("~", ENV["HOME"])
84
+ @config[:db].sub!("<network>", Bitcoin.network_name.to_s)
85
+ adapter = SEQUEL_ADAPTERS[@config[:db].split(":").first] rescue nil
86
+ Bitcoin.require_dependency(adapter, gem: adapter) if adapter
87
+ connect
88
+ end
89
+
90
+ # connect to database
91
+ def connect
92
+ Sequel.extension(:core_extensions, :sequel_3_dataset_methods)
93
+ @db = Sequel.connect(@config[:db].sub("~", ENV["HOME"]))
94
+ @db.extend_datasets(Sequel::Sequel3DatasetMethods)
95
+ sqlite_pragmas; migrate; check_metadata
96
+ log.info { "opened #{backend_name} store #{@db.uri}" }
97
+ end
98
+
99
+ # check if schema is up to date and migrate to current version if necessary
100
+ def migrate
101
+ migrations_path = File.join(File.dirname(__FILE__), "#{backend_name}/migrations")
102
+ Sequel.extension :migration
103
+ unless Sequel::Migrator.is_current?(@db, migrations_path)
104
+ Sequel::Migrator.run(@db, migrations_path)
105
+ unless (v = @db[:schema_info].first) && v[:magic] && v[:backend]
106
+ @db[:schema_info].update(
107
+ magic: Bitcoin.network[:magic_head].hth, backend: backend_name)
108
+ end
109
+ end
110
+ end
111
+
112
+ # check that database network magic and backend match the ones we are using
113
+ def check_metadata
114
+ version = @db[:schema_info].first
115
+ unless version[:magic] == Bitcoin.network[:magic_head].hth
116
+ name = Bitcoin::NETWORKS.find{|n,d| d[:magic_head].hth == version[:magic]}[0]
117
+ raise "Error: DB #{@db.url} was created for '#{name}' network!"
118
+ end
119
+ unless version[:backend] == backend_name
120
+ if version[:backend] == "sequel" && backend_name == "utxo"
121
+ log.warn { "Note: The 'utxo' store is now the default backend.
122
+ To keep using the full storage, change the configuration to use storage: 'sequel::#{@db.url}'.
123
+ To use the new storage backend, delete or move #{@db.url}, or specify a different database path in the config." }
124
+ end
125
+ raise "Error: DB #{@db.url} was created for '#{version[:backend]}' backend!"
126
+ end
127
+ end
128
+
129
+ # set pragma options for sqlite (if it is sqlite)
130
+ def sqlite_pragmas
131
+ return unless (@db.is_a?(Sequel::SQLite::Database) rescue false)
132
+ @config[:sqlite_pragmas].each do |name, value|
133
+ @db.pragma_set name, value
134
+ log.debug { "set sqlite pragma #{name} to #{value}" }
135
+ end
136
+ end
137
+
138
+ # name of the storage backend currently in use ("sequel" or "utxo")
139
+ def backend_name
140
+ self.class.name.split("::")[-1].split("Store")[0].downcase
49
141
  end
50
142
 
51
143
  # reset the store; delete all data
@@ -53,6 +145,16 @@ module Bitcoin::Storage
53
145
  raise "Not implemented"
54
146
  end
55
147
 
148
+ # handle a new block incoming from the network
149
+ def new_block blk
150
+ time = Time.now
151
+ res = store_block(blk)
152
+ log.info { "block #{blk.hash} " +
153
+ "[#{res[0]}, #{['main', 'side', 'orphan'][res[1]]}] " +
154
+ "(#{"%.4fs, %3dtx, %.3fkb" % [(Time.now - time), blk.tx.size, blk.payload.bytesize.to_f/1000]})" } if res && res[1]
155
+ res
156
+ end
157
+
56
158
  # store given block +blk+.
57
159
  # determine branch/chain and dept of block. trigger reorg if side branch becomes longer
58
160
  # than current main chain and connect orpans.
@@ -60,9 +162,17 @@ module Bitcoin::Storage
60
162
  log.debug { "new block #{blk.hash}" }
61
163
 
62
164
  existing = get_block(blk.hash)
63
- return [existing.depth, existing.chain] if existing && existing.chain == MAIN
165
+ if existing && existing.chain == MAIN
166
+ log.debug { "=> exists (#{existing.depth}, #{existing.chain})" }
167
+ return [existing.depth]
168
+ end
169
+
170
+ prev_block = get_block(blk.prev_block.reverse_hth)
171
+ unless @config[:skip_validation]
172
+ validator = blk.validator(self, prev_block)
173
+ validator.validate(rules: [:syntax], raise_errors: true)
174
+ end
64
175
 
65
- prev_block = get_block(hth(blk.prev_block.reverse))
66
176
  if !prev_block || prev_block.chain == ORPHAN
67
177
  if blk.hash == Bitcoin.network[:genesis_hash]
68
178
  log.debug { "=> genesis (0)" }
@@ -70,24 +180,38 @@ module Bitcoin::Storage
70
180
  else
71
181
  depth = prev_block ? prev_block.depth + 1 : 0
72
182
  log.debug { "=> orphan (#{depth})" }
183
+ return [0, 2] unless in_sync?
73
184
  return persist_block(blk, ORPHAN, depth)
74
185
  end
75
186
  end
76
187
  depth = prev_block.depth + 1
188
+
189
+ checkpoint = @checkpoints[depth]
190
+ if checkpoint && blk.hash != checkpoint
191
+ log.warn "Block #{depth} doesn't match checkpoint #{checkpoint}"
192
+ exit if depth > get_depth # TODO: handle checkpoint mismatch properly
193
+ end
77
194
  if prev_block.chain == MAIN
78
- next_block = prev_block.get_next_block
79
- if next_block && next_block.chain == MAIN
80
- log.debug { "=> side (#{depth})" }
81
- return persist_block(blk, SIDE, depth)
82
- else
195
+ if prev_block == get_head
83
196
  log.debug { "=> main (#{depth})" }
84
- return persist_block(blk, MAIN, depth)
197
+ if !@config[:skip_validation] && ( !@checkpoints.any? || depth > @checkpoints.keys.last )
198
+ if self.class.name =~ /UtxoStore/
199
+ @config[:utxo_cache] = 0
200
+ @config[:block_cache] = 120
201
+ end
202
+ validator.validate(rules: [:context], raise_errors: true)
203
+ end
204
+ return persist_block(blk, MAIN, depth, prev_block.work)
205
+ else
206
+ log.debug { "=> side (#{depth})" }
207
+ return persist_block(blk, SIDE, depth, prev_block.work)
85
208
  end
86
209
  else
87
210
  head = get_head
88
- if prev_block.depth + 1 <= head.depth
211
+ if prev_block.work + blk.block_work <= head.work
89
212
  log.debug { "=> side (#{depth})" }
90
- return persist_block(blk, SIDE, depth)
213
+ validator.validate(rules: [:context], raise_errors: true) unless @config[:skip_validation]
214
+ return persist_block(blk, SIDE, depth, prev_block.work)
91
215
  else
92
216
  log.debug { "=> reorg" }
93
217
  new_main, new_side = [], []
@@ -102,8 +226,8 @@ module Bitcoin::Storage
102
226
  end
103
227
  log.debug { "new main: #{new_main.inspect}" }
104
228
  log.debug { "new side: #{new_side.inspect}" }
105
- update_blocks([[new_main, {:chain => MAIN}], [new_side, {:chain => SIDE}]])
106
- return persist_block(blk, MAIN, depth)
229
+ reorg(new_side.reverse, new_main.reverse)
230
+ return persist_block(blk, MAIN, depth, prev_block.work)
107
231
  end
108
232
  end
109
233
  end
@@ -114,13 +238,17 @@ module Bitcoin::Storage
114
238
  end
115
239
 
116
240
  # update +attrs+ for block with given +hash+.
117
- # typically used to update chain.
241
+ # typically used to update the chain value during reorg.
118
242
  def update_block(hash, attrs)
119
243
  raise "Not implemented"
120
244
  end
121
245
 
246
+ def new_tx(tx)
247
+ store_tx(tx)
248
+ end
249
+
122
250
  # store given +tx+
123
- def store_tx(tx)
251
+ def store_tx(tx, validate = true)
124
252
  raise "Not implemented"
125
253
  end
126
254
 
@@ -146,7 +274,14 @@ module Bitcoin::Storage
146
274
 
147
275
  # compute blockchain locator
148
276
  def get_locator pointer = get_head
149
- return [Bitcoin::hth("\x00"*32)] if get_depth == -1
277
+ if @locator
278
+ locator, head = @locator
279
+ if head == get_head
280
+ return locator
281
+ end
282
+ end
283
+
284
+ return [("\x00"*32).hth] if get_depth == -1
150
285
  locator = []
151
286
  step = 1
152
287
  while pointer && pointer.hash != Bitcoin::network[:genesis_hash]
@@ -159,6 +294,7 @@ module Bitcoin::Storage
159
294
  step *= 2 if locator.size > 10
160
295
  end
161
296
  locator << Bitcoin::network[:genesis_hash]
297
+ @locator = [locator, get_head]
162
298
  locator
163
299
  end
164
300
 
@@ -203,6 +339,11 @@ module Bitcoin::Storage
203
339
  raise "Not implemented"
204
340
  end
205
341
 
342
+ # Grab the position of a tx in a given block
343
+ def get_idx_from_tx_hash(tx_hash)
344
+ raise "Not implemented"
345
+ end
346
+
206
347
  # collect all txouts containing the
207
348
  # given +script+
208
349
  def get_txouts_for_pk_script(script)
@@ -225,19 +366,83 @@ module Bitcoin::Storage
225
366
  nil
226
367
  end
227
368
 
369
+
370
+ # store address +hash160+
371
+ def store_addr(txout_id, hash160)
372
+ addr = @db[:addr][:hash160 => hash160]
373
+ addr_id = addr[:id] if addr
374
+ addr_id ||= @db[:addr].insert({:hash160 => hash160})
375
+ @db[:addr_txout].insert({:addr_id => addr_id, :txout_id => txout_id})
376
+ end
377
+
378
+ # parse script and collect address/txout mappings to index
379
+ def parse_script txout, i
380
+ addrs, names = [], []
381
+ # skip huge script in testnet3 block 54507 (998000 bytes)
382
+ return [SCRIPT_TYPES.index(:unknown), [], []] if txout.pk_script.bytesize > 10_000
383
+ script = Bitcoin::Script.new(txout.pk_script) rescue nil
384
+ if script
385
+ if script.is_hash160? || script.is_pubkey?
386
+ addrs << [i, script.get_hash160]
387
+ elsif script.is_multisig?
388
+ script.get_multisig_pubkeys.map do |pubkey|
389
+ addrs << [i, Bitcoin.hash160(pubkey.unpack("H*")[0])]
390
+ end
391
+ elsif Bitcoin.namecoin? && script.is_namecoin?
392
+ addrs << [i, script.get_hash160]
393
+ names << [i, script]
394
+ else
395
+ log.debug { "Unknown script type"}# #{tx.hash}:#{txout_idx}" }
396
+ end
397
+ script_type = SCRIPT_TYPES.index(script.type)
398
+ else
399
+ log.error { "Error parsing script"}# #{tx.hash}:#{txout_idx}" }
400
+ script_type = SCRIPT_TYPES.index(:unknown)
401
+ end
402
+ [script_type, addrs, names]
403
+ end
404
+
405
+ def add_watched_address address
406
+ hash160 = Bitcoin.hash160_from_address(address)
407
+ @db[:addr].insert(hash160: hash160) unless @db[:addr][hash160: hash160]
408
+ @watched_addrs << hash160 unless @watched_addrs.include?(hash160)
409
+ end
410
+
411
+ def rescan
412
+ raise "Not implemented"
413
+ end
414
+
228
415
  # import satoshi bitcoind blk0001.dat blockchain file
229
416
  def import filename, max_depth = nil
230
- File.open(filename) do |file|
231
- until file.eof?
232
- magic = file.read(4)
233
- raise "invalid network magic" unless Bitcoin.network[:magic_head] == magic
234
- size = file.read(4).unpack("L")[0]
235
- blk = Bitcoin::P::Block.new(file.read(size))
236
- depth, chain = store_block(blk)
237
- break if max_depth && depth >= max_depth
417
+ if File.file?(filename)
418
+ log.info { "Importing #{filename}" }
419
+ File.open(filename) do |file|
420
+ until file.eof?
421
+ magic = file.read(4)
422
+ raise "invalid network magic" unless Bitcoin.network[:magic_head] == magic
423
+ size = file.read(4).unpack("L")[0]
424
+ blk = Bitcoin::P::Block.new(file.read(size))
425
+ depth, chain = new_block(blk)
426
+ break if max_depth && depth >= max_depth
427
+ end
238
428
  end
429
+ elsif File.directory?(filename)
430
+ Dir.entries(filename).sort.each do |file|
431
+ next unless file =~ /^blk.*?\.dat$/
432
+ import(File.join(filename, file), max_depth)
433
+ end
434
+ else
435
+ raise "Import dir/file #{filename} not found"
239
436
  end
240
437
  end
438
+
439
+ def in_sync?
440
+ (get_head && (Time.now - get_head.time).to_i < 3600) ? true : false
441
+ end
442
+
241
443
  end
242
444
  end
243
445
  end
446
+
447
+ # TODO: someday sequel will support #blob directly and #to_sequel_blob will be gone
448
+ class String; def blob; to_sequel_blob; end; end
@@ -0,0 +1,52 @@
1
+ Sequel.migration do
2
+
3
+
4
+ up do
5
+
6
+ $stdout.puts "Running migration #{__FILE__}"
7
+
8
+ binary = adapter_scheme == :postgres ? :bytea : :varchar
9
+
10
+ alter_table :schema_info do
11
+ add_column :magic, :varchar # network magic-head
12
+ add_column :backend, :varchar # storage backend
13
+ end
14
+
15
+ next if tables.include?(:blk)
16
+
17
+ create_table :blk do
18
+ primary_key :id
19
+ column :hash, binary, :null => false, :unique => true, :index => true
20
+ column :depth, :int, :null => false, :index => true
21
+ column :version, :bigint, :null => false
22
+ column :prev_hash, binary, :null => false, :index => true
23
+ column :mrkl_root, binary, :null => false
24
+ column :time, :bigint, :null => false
25
+ column :bits, :bigint, :null => false
26
+ column :nonce, :bigint, :null => false
27
+ column :blk_size, :int, :null => false
28
+ column :chain, :int, :null => false
29
+ column :work, binary, :index => true
30
+ column :aux_pow, binary
31
+ end
32
+
33
+ create_table :addr do
34
+ primary_key :id
35
+ column :hash160, String, :null => false, :index => true
36
+ end
37
+
38
+ create_table :addr_txout do
39
+ column :addr_id, :int, :null => false, :index => true
40
+ column :txout_id, :int, :null => false, :index => true
41
+ end
42
+
43
+ create_table :names do
44
+ column :txout_id, :int, :null => false, :index => true
45
+ column :hash, binary, :index => true
46
+ column :name, binary, :index => true
47
+ column :value, binary
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,18 @@
1
+ Sequel.migration do
2
+
3
+ up do
4
+
5
+ $stdout.puts "Running migration #{__FILE__}"
6
+
7
+ create_table :utxo do
8
+ primary_key :id
9
+ column :tx_hash, String, null: false, index: true
10
+ column :tx_idx, :int, null: false, index: true
11
+ column :blk_id, :int, null: false, index: true
12
+ column :pk_script, (@db.adapter_scheme == :postgres ? :bytea : :blob), null: false
13
+ column :value, :bigint, null: false, index: true
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,361 @@
1
+ Bitcoin.require_dependency :sequel, message:
2
+ "Note: You will also need an adapter for your database like sqlite3, mysql2, postgresql"
3
+
4
+ module Bitcoin::Storage::Backends
5
+
6
+ # Storage backend using Sequel to connect to arbitrary SQL databases.
7
+ # Inherits from StoreBase and implements its interface.
8
+ class UtxoStore < StoreBase
9
+
10
+
11
+ # possible script types
12
+ SCRIPT_TYPES = [:unknown, :pubkey, :hash160, :multisig, :p2sh]
13
+ if Bitcoin.namecoin?
14
+ [:name_new, :name_firstupdate, :name_update].each {|n| SCRIPT_TYPES << n }
15
+ end
16
+
17
+ # sequel database connection
18
+ attr_accessor :db
19
+
20
+ DEFAULT_CONFIG = {
21
+ # cache head block; it is only updated when new block comes in,
22
+ # so this should only be used by the store receiving new blocks.
23
+ cache_head: false,
24
+ # cache this many utxo records before syncing to disk.
25
+ # this should only be enabled during initial sync, because
26
+ # with it the store cannot reorg properly.
27
+ utxo_cache: 250,
28
+ # cache this many blocks.
29
+ # NOTE: this is also the maximum number of blocks the store can reorg.
30
+ block_cache: 120,
31
+ # keep an index of utxos for all addresses, not just the ones
32
+ # we are explicitly told about.
33
+ index_all_addrs: false
34
+ }
35
+
36
+ # create sequel store with given +config+
37
+ def initialize config, *args
38
+ super config, *args
39
+ @spent_outs, @new_outs, @watched_addrs = [], [], []
40
+ @deleted_utxos, @tx_cache, @block_cache = {}, {}, {}
41
+ end
42
+
43
+ # connect to database
44
+ def connect
45
+ super
46
+ load_watched_addrs
47
+ # rescan
48
+
49
+ end
50
+
51
+ # reset database; delete all data
52
+ def reset
53
+ [:blk, :utxo, :addr, :addr_txout].each {|table| @db[table].delete }
54
+ @head = nil
55
+ end
56
+
57
+ # persist given block +blk+ to storage.
58
+ def persist_block blk, chain, depth, prev_work = 0
59
+ load_watched_addrs
60
+ @db.transaction do
61
+ attrs = {
62
+ :hash => blk.hash.htb.blob,
63
+ :depth => depth,
64
+ :chain => chain,
65
+ :version => blk.ver,
66
+ :prev_hash => blk.prev_block.reverse.blob,
67
+ :mrkl_root => blk.mrkl_root.reverse.blob,
68
+ :time => blk.time,
69
+ :bits => blk.bits,
70
+ :nonce => blk.nonce,
71
+ :blk_size => blk.payload.bytesize,
72
+ :work => (prev_work + blk.block_work).to_s
73
+ }
74
+ existing = @db[:blk].filter(:hash => blk.hash.htb.blob)
75
+ if existing.any?
76
+ existing.update attrs
77
+ block_id = existing.first[:id]
78
+ else
79
+ block_id = @db[:blk].insert(attrs)
80
+ end
81
+
82
+ if @config[:block_cache] > 0
83
+ @block_cache.shift if @block_cache.size > @config[:block_cache]
84
+ @deleted_utxos.shift if @deleted_utxos.size > @config[:block_cache]
85
+ @block_cache[blk.hash] = blk
86
+ end
87
+
88
+ if chain == MAIN
89
+ persist_transactions(blk.tx, block_id, depth)
90
+ @tx_cache = {}
91
+ @head = wrap_block(attrs.merge(id: block_id)) if chain == MAIN
92
+ end
93
+ return depth, chain
94
+ end
95
+ end
96
+
97
+ def persist_transactions txs, block_id, depth
98
+ txs.each.with_index do |tx, tx_blk_idx|
99
+ tx.in.each.with_index do |txin, txin_tx_idx|
100
+ next if txin.coinbase?
101
+ size = @new_outs.size
102
+ @new_outs.delete_if {|o| o[0][:tx_hash] == txin.prev_out.reverse.hth &&
103
+ o[0][:tx_idx] == txin.prev_out_index }
104
+ @spent_outs << {
105
+ tx_hash: txin.prev_out.reverse.hth.to_sequel_blob,
106
+ tx_idx: txin.prev_out_index } if @new_outs.size == size
107
+ end
108
+ tx.out.each.with_index do |txout, txout_tx_idx|
109
+ _, a, n = *parse_script(txout, txout_tx_idx)
110
+ @new_outs << [{
111
+ :tx_hash => tx.hash.blob,
112
+ :tx_idx => txout_tx_idx,
113
+ :blk_id => block_id,
114
+ :pk_script => txout.pk_script.blob,
115
+ :value => txout.value },
116
+ @config[:index_all_addrs] ? a : a.select {|a| @watched_addrs.include?(a[1]) },
117
+ Bitcoin.namecoin? ? n : [] ]
118
+ end
119
+ end
120
+ flush_spent_outs(depth) if @spent_outs.size > @config[:utxo_cache]
121
+ flush_new_outs(depth) if @new_outs.size > @config[:utxo_cache]
122
+ end
123
+
124
+ def reorg new_side, new_main
125
+ new_side.each do |block_hash|
126
+ raise "trying to remove non-head block!" unless get_head.hash == block_hash
127
+ depth = get_depth
128
+ blk = @db[:blk][hash: block_hash.htb.blob]
129
+ delete_utxos = @db[:utxo].where(blk_id: blk[:id])
130
+ @db[:addr_txout].where("txout_id IN ?", delete_utxos.map{|o|o[:id]}).delete
131
+
132
+ delete_utxos.delete
133
+ (@deleted_utxos[depth] || []).each do |utxo|
134
+ utxo[:pk_script] = utxo[:pk_script].to_sequel_blob
135
+ utxo_id = @db[:utxo].insert(utxo)
136
+ addrs = Bitcoin::Script.new(utxo[:pk_script]).get_addresses
137
+ addrs.each do |addr|
138
+ hash160 = Bitcoin.hash160_from_address(addr)
139
+ store_addr(utxo_id, hash160)
140
+ end
141
+ end
142
+
143
+ @db[:blk].where(id: blk[:id]).update(chain: SIDE)
144
+ end
145
+
146
+ new_main.each do |block_hash|
147
+ block = @db[:blk][hash: block_hash.htb.blob]
148
+ blk = @block_cache[block_hash]
149
+ persist_transactions(blk.tx, block[:id], block[:depth])
150
+ @db[:blk].where(id: block[:id]).update(chain: MAIN)
151
+ end
152
+ end
153
+
154
+ def flush_spent_outs depth
155
+ log.time "flushed #{@spent_outs.size} spent txouts in %.4fs" do
156
+ if @spent_outs.any?
157
+ @spent_outs.each_slice(250) do |slice|
158
+ if @db.adapter_scheme == :postgres
159
+ condition = slice.map {|o| "(tx_hash = '#{o[:tx_hash]}' AND tx_idx = #{o[:tx_idx]})" }.join(" OR ")
160
+ else
161
+ condition = slice.map {|o| "(tx_hash = X'#{o[:tx_hash].hth}' AND tx_idx = #{o[:tx_idx]})" }.join(" OR ")
162
+ end
163
+ @db["DELETE FROM addr_txout WHERE EXISTS
164
+ (SELECT 1 FROM utxo WHERE
165
+ utxo.id = addr_txout.txout_id AND (#{condition}));"].all
166
+ @db["DELETE FROM utxo WHERE #{condition};"].first
167
+
168
+ end
169
+ end
170
+ @spent_outs = []
171
+ end
172
+ end
173
+
174
+ def flush_new_outs depth
175
+ log.time "flushed #{@new_outs.size} new txouts in %.4fs" do
176
+ new_utxo_ids = @db[:utxo].insert_multiple(@new_outs.map{|o|o[0]})
177
+ @new_outs.each.with_index do |d, idx|
178
+ d[1].each do |i, hash160|
179
+ next unless i && hash160
180
+ store_addr(new_utxo_ids[idx], hash160)
181
+ end
182
+ end
183
+
184
+ @new_outs.each.with_index do |d, idx|
185
+ d[2].each do |i, script|
186
+ next unless i && script
187
+ store_name(script, new_utxo_ids[idx])
188
+ end
189
+ end
190
+ @new_outs = []
191
+ end
192
+ end
193
+
194
+ def add_watched_address address
195
+ hash160 = Bitcoin.hash160_from_address(address)
196
+ @db[:addr].insert(hash160: hash160) unless @db[:addr][hash160: hash160]
197
+ @watched_addrs << hash160 unless @watched_addrs.include?(hash160)
198
+ end
199
+
200
+ def load_watched_addrs
201
+ @watched_addrs = @db[:addr].all.map{|a| a[:hash160] } unless @config[:index_all_addrs]
202
+ end
203
+
204
+ def rescan
205
+ load_watched_addrs
206
+ @rescan_lock ||= Monitor.new
207
+ @rescan_lock.synchronize do
208
+ log.info { "Rescanning #{@db[:utxo].count} utxos for #{@watched_addrs.size} addrs" }
209
+ count = @db[:utxo].count; n = 100_000
210
+ @db[:utxo].order(:id).each_slice(n).with_index do |slice, index|
211
+ log.debug { "rescan progress: %.2f%" % (100.0 / count * (index*n)) }
212
+ slice.each do |utxo|
213
+ next if utxo[:pk_script].bytesize >= 10_000
214
+ hash160 = Bitcoin::Script.new(utxo[:pk_script]).get_hash160
215
+ if @config[:index_all_addrs] || @watched_addrs.include?(hash160)
216
+ log.info { "Found utxo for address #{Bitcoin.hash160_to_address(hash160)}: " +
217
+ "#{utxo[:tx_hash][0..8]}:#{utxo[:tx_idx]} (#{utxo[:value]})" }
218
+ addr = @db[:addr][hash160: hash160]
219
+ addr_utxo = {addr_id: addr[:id], txout_id: utxo[:id]}
220
+ @db[:addr_txout].insert(addr_utxo) unless @db[:addr_txout][addr_utxo]
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ # check if block +blk_hash+ exists
228
+ def has_block(blk_hash)
229
+ !!@db[:blk].where(:hash => blk_hash.htb.blob).get(1)
230
+ end
231
+
232
+ # check if transaction +tx_hash+ exists
233
+ def has_tx(tx_hash)
234
+ !!@db[:utxo].where(:tx_hash => tx_hash.blob).get(1)
235
+ end
236
+
237
+ # get head block (highest block from the MAIN chain)
238
+ def get_head
239
+ (@config[:cache_head] && @head) ? @head :
240
+ @head = wrap_block(@db[:blk].filter(:chain => MAIN).order(:depth).last)
241
+ end
242
+
243
+ # get depth of MAIN chain
244
+ def get_depth
245
+ return -1 unless get_head
246
+ get_head.depth
247
+ end
248
+
249
+ # get block for given +blk_hash+
250
+ def get_block(blk_hash)
251
+ wrap_block(@db[:blk][:hash => blk_hash.htb.blob])
252
+ end
253
+
254
+ # get block by given +depth+
255
+ def get_block_by_depth(depth)
256
+ wrap_block(@db[:blk][:depth => depth, :chain => MAIN])
257
+ end
258
+
259
+ # get block by given +prev_hash+
260
+ def get_block_by_prev_hash(prev_hash)
261
+ wrap_block(@db[:blk][:prev_hash => prev_hash.htb.blob, :chain => MAIN])
262
+ end
263
+
264
+ # get block by given +tx_hash+
265
+ def get_block_by_tx(tx_hash)
266
+ block_id = @db[:utxo][tx_hash: tx_hash.blob][:blk_id]
267
+ get_block_by_id(block_id)
268
+ end
269
+
270
+ # get block by given +id+
271
+ def get_block_by_id(block_id)
272
+ wrap_block(@db[:blk][:id => block_id])
273
+ end
274
+
275
+ # get transaction for given +tx_hash+
276
+ def get_tx(tx_hash)
277
+ @tx_cache[tx_hash] ||= wrap_tx(tx_hash)
278
+ end
279
+
280
+ # get transaction by given +tx_id+
281
+ def get_tx_by_id(tx_id)
282
+ get_tx(tx_id)
283
+ end
284
+
285
+ def get_txout_by_id(id)
286
+ wrap_txout(@db[:utxo][id: id])
287
+ end
288
+
289
+ # get corresponding Models::TxOut for +txin+
290
+ def get_txout_for_txin(txin)
291
+ wrap_txout(@db[:utxo][tx_hash: txin.prev_out.reverse.hth.blob, tx_idx: txin.prev_out_index])
292
+ end
293
+
294
+ # get all Models::TxOut matching given +script+
295
+ def get_txouts_for_pk_script(script)
296
+ utxos = @db[:utxo].filter(pk_script: script.blob).order(:blk_id)
297
+ utxos.map {|utxo| wrap_txout(utxo) }
298
+ end
299
+
300
+ # get all Models::TxOut matching given +hash160+
301
+ def get_txouts_for_hash160(hash160, unconfirmed = false)
302
+ addr = @db[:addr][hash160: hash160]
303
+ return [] unless addr
304
+ @db[:addr_txout].where(addr_id: addr[:id]).map {|ao| wrap_txout(@db[:utxo][id: ao[:txout_id]]) }.compact
305
+ end
306
+
307
+ def get_balance hash160
308
+ get_txouts_for_hash160(hash160).map(&:value).inject(:+) || 0
309
+ end
310
+
311
+ # wrap given +block+ into Models::Block
312
+ def wrap_block(block)
313
+ return nil unless block
314
+
315
+ data = {:id => block[:id], :depth => block[:depth], :chain => block[:chain],
316
+ :work => block[:work].to_i, :hash => block[:hash].hth}
317
+ blk = Bitcoin::Storage::Models::Block.new(self, data)
318
+
319
+ blk.ver = block[:version]
320
+ blk.prev_block = block[:prev_hash].reverse
321
+ blk.mrkl_root = block[:mrkl_root].reverse
322
+ blk.time = block[:time].to_i
323
+ blk.bits = block[:bits]
324
+ blk.nonce = block[:nonce]
325
+
326
+ if cached = @block_cache[block[:hash].hth]
327
+ blk.tx = cached.tx
328
+ end
329
+
330
+ blk.recalc_block_hash
331
+ blk
332
+ end
333
+
334
+ # wrap given +transaction+ into Models::Transaction
335
+ def wrap_tx(tx_hash)
336
+ utxos = @db[:utxo].where(tx_hash: tx_hash.blob)
337
+ return nil unless utxos.any?
338
+ data = { blk_id: utxos.first[:blk_id] }
339
+ tx = Bitcoin::Storage::Models::Tx.new(self, data)
340
+ tx.hash = tx_hash # utxos.first[:tx_hash].hth
341
+ utxos.each {|u| tx.out[u[:tx_idx]] = wrap_txout(u) }
342
+ return tx
343
+ end
344
+
345
+ # wrap given +output+ into Models::TxOut
346
+ def wrap_txout(utxo)
347
+ return nil unless utxo
348
+ data = {id: utxo[:id], tx_id: utxo[:tx_hash], tx_idx: utxo[:tx_idx]}
349
+ txout = Bitcoin::Storage::Models::TxOut.new(self, data)
350
+ txout.value = utxo[:value]
351
+ txout.pk_script = utxo[:pk_script]
352
+ txout
353
+ end
354
+
355
+ def check_consistency(*args)
356
+ log.warn { "Utxo store doesn't support consistency check" }
357
+ end
358
+
359
+ end
360
+
361
+ end