bitcoin-ruby 0.0.1 → 0.0.2

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 (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