├── .gitignore ├── .travis.yml ├── Gemfile ├── README.md ├── bcwallet.rb └── test_bcwallet.rb /.gitignore: -------------------------------------------------------------------------------- 1 | keys_testnet 2 | data_testnet 3 | keys 4 | data 5 | Gemfile.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - ruby-head 4 | - 2.0.0 5 | 6 | install: 7 | - gem install coveralls 8 | - gem install minitest 9 | 10 | script: 11 | - ruby test_bcwallet.rb 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem "minitest" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bcwallet.rb: A stand-alone Bitcoin client within approx. 800 LOC! 2 | 3 | bcwallet.rb is an educational Bitcoin client written in Ruby language. 4 | 5 | The client is written for [Bitcoin no shikumi](http://bitcoin.peryaudo.org/) ("The Mechanism of Bitcoin"), a Japanese website describes Bitcoin from the technical perspective. 6 | 7 | [![Build Status](https://travis-ci.org/peryaudo/bcwallet.svg?branch=master)](https://travis-ci.org/peryaudo/bcwallet) [![Code Climate](https://codeclimate.com/github/peryaudo/bcwallet.png)](https://codeclimate.com/github/peryaudo/bcwallet) [![Coverage Status](https://coveralls.io/repos/peryaudo/bcwallet/badge.png?branch=master)](https://coveralls.io/r/peryaudo/bcwallet?branch=master) 8 | 9 | ## Features 10 | 11 | * Stand-alone Bitcoin client within approx. 800 lines of code (counted by CLOC) 12 | * Generate address, export key, show balances for addresses, send coins from addresses ... 13 | * Written in pure Ruby 14 | * No additional dependencies 15 | * Implements Simplified Payment Verification (SPV) 16 | * Comments to help you understand how Bitcoin client is implemented 17 | * Testnet supported (use in main network is not recommended) 18 | 19 | ## Usage 20 | 21 | Usage: ruby bcwallet.rb [] 22 | commands: 23 | generate generate a new Bitcoin address 24 | list show list for all Bitcoin addresses 25 | export show private key for the Bitcoin address 26 | balance show balances for all Bitcoin addresses 27 | send transfer coins to the Bitcoin address 28 | 29 | ## Dependencies 30 | 31 | * Ruby >= 2.0.0 32 | 33 | ## Disclaimer 34 | 35 | DO NOT USE THIS CLIENT IN MAIN NETWORK, OR YOU MAY LOSE YOUR COINS! 36 | 37 | Because this client is for technical education, it skips a lot of validations and may have critical bugs. 38 | 39 | Use [Testnet](https://en.bitcoin.it/wiki/Testnet) instead. Testnet coins are worthless coins. You can receive Testnet coins for free at [TP's TestNet Faucet](http://tpfaucet.appspot.com/) to try this client. 40 | 41 | ## License 42 | 43 | The MIT License (MIT) 44 | 45 | Copyright (c) 2014 peryaudo 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy 48 | of this software and associated documentation files (the "Software"), to deal 49 | in the Software without restriction, including without limitation the rights 50 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 51 | copies of the Software, and to permit persons to whom the Software is 52 | furnished to do so, subject to the following conditions: 53 | 54 | The above copyright notice and this permission notice shall be included in 55 | all copies or substantial portions of the Software. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 58 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 59 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 60 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 61 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 62 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 63 | THE SOFTWARE. 64 | 65 | ## ToDo 66 | 67 | * Write Merkle tree validation for transactions 68 | * Rewrite with Fiber 69 | -------------------------------------------------------------------------------- /bcwallet.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # bcwallet.rb: Educational Bitcoin Client 5 | # 6 | # This is a tiny Bitcoin client implementation which uses 7 | # Simplified Payment Verification (SPV). 8 | # 9 | 10 | # WARNING: This client is for technical education, 11 | # skips a lot of validations, and may have critical bugs. 12 | # 13 | # USE OF THE CLIENT IN MAIN NETWORK MAY CAUSE YOUR COINS LOST. 14 | # 15 | # DO NOT SET THIS VALUE "false". 16 | # 17 | IS_TESTNET = true 18 | 19 | # Remote host to use: It is recommended to use this client with a local client. 20 | # Install Bitcoin-Qt and then launch with -testnet option to connect Testnet. 21 | HOST = 'localhost' 22 | 23 | # This software is licensed under the MIT license. 24 | # 25 | # The MIT License (MIT) 26 | # 27 | # Copyright (c) 2014 peryaudo 28 | # 29 | # Permission is hereby granted, free of charge, to any person obtaining a copy 30 | # of this software and associated documentation files (the "Software"), to deal 31 | # in the Software without restriction, including without limitation the rights 32 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | # copies of the Software, and to permit persons to whom the Software is 34 | # furnished to do so, subject to the following conditions: 35 | # 36 | # The above copyright notice and this permission notice shall be included in 37 | # all copies or substantial portions of the Software. 38 | # 39 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 45 | # THE SOFTWARE. 46 | # 47 | 48 | require 'openssl' 49 | require 'socket' 50 | 51 | # 52 | # A class which manages both public key and private key in OpenSSL's ECDSA. 53 | # 54 | class Key 55 | # 56 | # Bitcoin mainly uses SHA-256(SHA-256(plain)) as a cryptographic hash function 57 | # when a hash is needed. 58 | # 59 | def self.hash256(plain) 60 | OpenSSL::Digest::SHA256.digest(OpenSSL::Digest::SHA256.digest(plain)) 61 | end 62 | 63 | # 64 | # RIPEMD-160(SHA-256(plain)) is used when a shorter hash is preferable. 65 | # 66 | def self.hash160(plain) 67 | OpenSSL::Digest::RIPEMD160.digest(OpenSSL::Digest::SHA256.digest(plain)) 68 | end 69 | 70 | BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 71 | 72 | # 73 | # Modified version of Base58 is used in Bitcoin to convert binaries into human-typable strings. 74 | # 75 | def self.encode_base58(plain) 76 | # plain is big endian 77 | 78 | num = plain.unpack("H*").first.hex 79 | 80 | res = '' 81 | 82 | while num > 0 83 | res += BASE58[num % 58] 84 | num /= 58 85 | end 86 | 87 | # restore leading zeroes 88 | plain.each_byte do |c| 89 | break if c != 0 90 | res += BASE58[0] 91 | end 92 | 93 | res.reverse 94 | end 95 | 96 | def self.decode_base58(encoded) 97 | num = 0 98 | encoded.each_char do |c| 99 | num *= 58 100 | num += BASE58.index(c) 101 | end 102 | 103 | res = num.to_s(16) 104 | 105 | if res.length % 2 == 1 106 | res = '0' + res 107 | end 108 | 109 | # restore leading zeroes 110 | encoded.each_char do |c| 111 | break if c != BASE58[0] 112 | res = '00' + res 113 | end 114 | 115 | [res].pack('H*') 116 | end 117 | 118 | # 119 | # Base58 with the type of data and the checksum is called Base58Check in Bitcoin protocol. 120 | # It is used as a Bitcoin address, human-readable private key, etc. 121 | # 122 | def self.encode_base58check(type, plain) 123 | leading_bytes = { 124 | main: { public_key: 0, private_key: 128 }, 125 | testnet: { public_key: 111, private_key: 239 } 126 | } 127 | 128 | leading_byte = [leading_bytes[IS_TESTNET ? :testnet : :main][type]].pack('C') 129 | 130 | data = leading_byte + plain 131 | checksum = Key.hash256(data)[0, 4] 132 | 133 | Key.encode_base58(data + checksum) 134 | end 135 | 136 | def self.decode_base58check(encoded) 137 | decoded = Key.decode_base58(encoded) 138 | 139 | raise "invalid base58 checksum" if Key.hash256(decoded[0, decoded.length - 4])[0, 4] != decoded[-4, 4] 140 | 141 | types = { 142 | main: { 0 => :public_key, 128 => :private_key }, 143 | testnet: { 111 => :public_key, 239 => :private_key } 144 | } 145 | 146 | type = types[IS_TESTNET ? :testnet : :main][decoded[0].unpack('C').first] 147 | 148 | { type: type, data: decoded[1, decoded.length - 5] } 149 | end 150 | 151 | # 152 | # Initialize with ASCII-encoded DER format string (nil to generate a new key) 153 | # 154 | def initialize(der = nil) 155 | if der 156 | @key = OpenSSL::PKey::EC.new([der].pack('H*')) 157 | else 158 | @key = OpenSSL::PKey::EC.new('secp256k1') 159 | @key = @key.generate_key 160 | end 161 | 162 | @key.check_key 163 | end 164 | 165 | # 166 | # Sign the data with the key. 167 | # 168 | def sign(data) 169 | @key.dsa_sign_asn1(data) 170 | end 171 | 172 | # 173 | # Convert public key to Bitcoin address. 174 | # 175 | def to_address_s 176 | Key.encode_base58check(:public_key, Key.hash160(@key.public_key.to_bn.to_s(2))) 177 | end 178 | 179 | # 180 | # Convert the private key to Bitcoin private key import format. 181 | # 182 | def to_private_key_s 183 | Key.encode_base58check(:private_key, @key.private_key.to_s(2)) 184 | end 185 | 186 | # 187 | # Convert the key pair into ASCII-encoded DER format string. 188 | # 189 | def to_der_hex_s 190 | @key.to_der.unpack('H*').first 191 | end 192 | 193 | def to_public_key 194 | @key.public_key.to_bn.to_s(2) 195 | end 196 | 197 | def to_public_key_hash 198 | Key.hash160(@key.public_key.to_bn.to_s(2)) 199 | end 200 | end 201 | 202 | # 203 | # A class which generates Bloom filter. 204 | # Bloom filter is a data structure used in Bitcoin to filter transactions for SPV clients. 205 | # It enables you to quickly test whether an element is included in a set, 206 | # but may have false positives (i.e. probabilistic data structure). 207 | # 208 | class BloomFilter 209 | public 210 | # 211 | # len = length of bloom filter 212 | # hash_funcs = number of hash functions to use 213 | # tweak = a random number 214 | # 215 | def initialize(len, hash_funcs, tweak) 216 | @filter = Array.new(len, 0) 217 | @hash_funcs = hash_funcs 218 | @tweak = tweak 219 | end 220 | 221 | # 222 | # See an array as a huge little endian integer, and fill idx-th bit 223 | # 224 | def set_bit(idx) 225 | @filter[idx >> 3] |= (1 << (7 & idx)) 226 | end 227 | 228 | def rotate_left_32(x, r) 229 | ((x << r) | (x >> (32 - r))) & 0xffffffff 230 | end 231 | 232 | # 233 | # The hash functions is called MurmurHash3 (32-bit). 234 | # Reference implementation is somewhat tricky one, 235 | # so I recommend you to read bitcoinj's one if you want to know the detail. 236 | # 237 | def hash(seed, data) 238 | mask = 0xffffffff 239 | 240 | h1 = seed & mask 241 | c1 = 0xcc9e2d51 242 | c2 = 0x1b873593 243 | 244 | data.unpack('V*').each do |k1| 245 | k1 = (k1 * c1) & mask 246 | k1 = rotate_left_32(k1, 15) 247 | k1 = (k1 * c2) & mask 248 | 249 | h1 = h1 ^ k1 250 | h1 = rotate_left_32(h1, 13) 251 | h1 = (h1 * 5 + 0xe6546b64) & mask 252 | end 253 | 254 | padded_remaining_bytes = data[(data.length & (mask ^ 3))..-1] + "\0" * (4 - (data.length & 3)) 255 | 256 | k1 = padded_remaining_bytes.unpack('V').first 257 | k1 = (k1 * c1) & mask 258 | k1 = rotate_left_32(k1, 15) 259 | k1 = (k1 * c2) & mask 260 | h1 = h1 ^ k1 261 | 262 | h1 = (h1 ^ data.length) & mask 263 | h1 = h1 ^ (h1 >> 16) 264 | h1 = (h1 * 0x85ebca6b) & mask 265 | h1 = h1 ^ (h1 >> 13) 266 | h1 = (h1 * 0xc2b2ae35) & mask 267 | h1 = h1 ^ (h1 >> 16) 268 | 269 | h1 270 | end 271 | 272 | # 273 | # Insert the data into the Bloom filter 274 | # 275 | def insert(data) 276 | @hash_funcs.times do |i| 277 | set_bit(hash(i * 0xfba4c795 + @tweak, data) % (@filter.length * 8)) 278 | end 279 | end 280 | 281 | def to_s 282 | res = '' 283 | @filter.each do |byte| 284 | res += [byte].pack('C') 285 | end 286 | res 287 | end 288 | end 289 | 290 | # 291 | # A class for message serializers and deserializers. 292 | # It contains type definitions of structures. 293 | # 294 | class Message 295 | # 296 | # Constants used in inventory vector 297 | # 298 | MSG_TX = 1 299 | MSG_BLOCK = 2 300 | MSG_FILTERED_BLOCK = 3 301 | 302 | 303 | def initialize 304 | # 305 | # Message definitions. 306 | # 307 | @message_definitions = { 308 | version: [ 309 | [:version, :uint32], 310 | [:services, :uint64], 311 | [:timestamp, :uint64], 312 | [:your_addr, :net_addr], 313 | [:my_addr, :net_addr], 314 | [:nonce, :uint64], 315 | [:agent, :string], 316 | [:height, :uint32], 317 | [:relay, :relay_flag] 318 | ], 319 | ping: [[:nonce, :uint64]], 320 | pong: [[:nonce, :uint64]], 321 | alert: [], 322 | verack: [], 323 | mempool: [], 324 | addr: [[:addr, array_for(:net_addr)]], 325 | inv: [[:inventory, array_for(:inv_vect)]], 326 | merkleblock: [ 327 | [:hash, :block_hash], 328 | [:version, :uint32], 329 | [:prev_block, :hash256], 330 | [:merkle_root, :hash256], 331 | [:timestamp, :uint32], 332 | [:bits, :uint32], 333 | [:nonce, :uint32], 334 | [:total_txs, :uint32], 335 | [:hashes, array_for(:hash256)], 336 | [:flags, :string] 337 | ], 338 | tx: [ 339 | [:hash, :tx_hash], 340 | [:version, :uint32], 341 | [:tx_in, array_for(:tx_in)], 342 | [:tx_out, array_for(:tx_out)], 343 | [:lock_time, :uint32] 344 | ], 345 | filterload: [ 346 | [:filter, :string], 347 | [:hash_funcs, :uint32], 348 | [:tweak, :uint32], 349 | [:flag, :uint8] 350 | ], 351 | getblocks: [ 352 | [:version, :uint32], 353 | [:block_locator, array_for(:hash256)], 354 | [:hash_stop, :hash256] 355 | ], 356 | getdata: [[:inventory, array_for(:inv_vect)]], 357 | inv_vect: [ 358 | [:type, :uint32], 359 | [:hash, :hash256]], 360 | outpoint: [ 361 | [:hash, :hash256], 362 | [:index, :uint32]], 363 | tx_in: [ 364 | [:previous_output, :outpoint], 365 | [:signature_script, :string], 366 | [:sequence, :uint32]], 367 | tx_out: [ 368 | [:value, :uint64], 369 | [:pk_script, :string]] 370 | } 371 | end 372 | 373 | # 374 | # Serialize a message using message definitions. 375 | # 376 | def serialize(message) 377 | @payload = '' 378 | 379 | serialize_struct(message[:command], message) 380 | 381 | @payload 382 | end 383 | 384 | # 385 | # Deserialize a message using message definitions. 386 | # 387 | def deserialize(command, payload) 388 | raise unless @message_definitions.has_key?(command) 389 | 390 | @payload = payload 391 | 392 | res = deserialize_struct(command) 393 | res[:command] = command 394 | 395 | res 396 | end 397 | 398 | # 399 | # Read a message and parse it using message definitions. 400 | # 401 | def read(socket) 402 | packet = read_packet(socket) 403 | 404 | expected_magic = [IS_TESTNET ? '0b110907' : 'f9beb4d9'].pack('H*') 405 | expected_checksum = Key.hash256(packet[:payload])[0, 4] 406 | 407 | if packet[:magic] != expected_magic 408 | raise 'invalid magic received' 409 | end 410 | 411 | if packet[:checksum] != expected_checksum 412 | raise 'incorrect checksum' 413 | end 414 | 415 | unless @message_definitions.has_key?(packet[:command]) 416 | raise 'invalid message type' 417 | end 418 | 419 | deserialize(packet[:command], packet[:payload]) 420 | end 421 | 422 | # 423 | # Actually send a message to the remote host. 424 | # 425 | def write(socket, message) 426 | # Create payload 427 | payload = serialize(message) 428 | 429 | # 4bytes: magic 430 | raw_message = [IS_TESTNET ? '0b110907' : 'f9beb4d9'].pack('H*') 431 | 432 | # 12bytes: command (padded with zeroes) 433 | raw_message += [message[:command].to_s].pack('a12') 434 | 435 | # 4bytes: length of payload 436 | raw_message += [payload.length].pack('V') 437 | 438 | # 4bytes: checksum 439 | raw_message += Key.hash256(payload)[0, 4] 440 | 441 | # payload 442 | raw_message += payload 443 | 444 | socket.write raw_message 445 | socket.flush 446 | end 447 | 448 | private 449 | 450 | def read_packet(socket) 451 | magic = socket.read(4) 452 | command = socket.read(12).unpack('A12').first.to_sym 453 | length = socket.read(4).unpack('V').first 454 | checksum = socket.read(4) 455 | payload = socket.read(length) 456 | 457 | { magic: magic, command: command, checksum: checksum, payload: payload } 458 | end 459 | 460 | def serialize_struct(type, struct) 461 | if type.kind_of?(Proc) 462 | type.call(:write, struct) 463 | return 464 | end 465 | 466 | if @message_definitions.has_key?(type) 467 | @message_definitions[type].each do |definition| 468 | next if struct.has_key?(:command) && definition.first == :hash 469 | serialize_struct(definition.last, struct[definition.first]) 470 | end 471 | else 472 | method(type).call(:write, struct) 473 | end 474 | end 475 | 476 | def deserialize_struct(type) 477 | if type.kind_of?(Proc) 478 | return type.call(:read) 479 | end 480 | 481 | if @message_definitions.has_key?(type) 482 | res = {} 483 | @message_definitions[type].each do |definition| 484 | res[definition.first] = deserialize_struct(definition.last) 485 | end 486 | res 487 | else 488 | method(type).call(:read) 489 | end 490 | end 491 | 492 | # 493 | # Higher order function to generate array serializer / deserializer 494 | # 495 | def array_for(elm) 496 | lambda do |rw, val = nil| 497 | case rw 498 | when :read 499 | count = integer(:read) 500 | res = [] 501 | count.times do 502 | res.push deserialize_struct(elm) 503 | end 504 | res 505 | when :write 506 | integer(:write, val.length) 507 | val.each do |v| 508 | serialize_struct(elm, v) 509 | end 510 | val 511 | end 512 | end 513 | end 514 | 515 | # 516 | # Serializer & deserializer methods 517 | # 518 | 519 | def read_bytes(len) 520 | res = @payload[0, len] 521 | @payload = @payload[len..-1] 522 | res 523 | end 524 | 525 | def write_bytes(val) 526 | @payload += val 527 | end 528 | 529 | def fixed_integer(templ, len, rw, val = nil) 530 | case rw 531 | when :read 532 | read_bytes(len).unpack(templ).first 533 | when :write 534 | write_bytes([val].pack(templ)) 535 | end 536 | end 537 | 538 | def uint8(rw, val = nil) 539 | fixed_integer('C', 1, rw, val) 540 | end 541 | 542 | def uint16(rw, val = nil) 543 | fixed_integer('v', 2, rw, val) 544 | end 545 | 546 | def uint32(rw, val = nil) 547 | fixed_integer('V', 4, rw, val) 548 | end 549 | 550 | def uint64(rw, val = nil) 551 | fixed_integer('Q', 8, rw, val) 552 | end 553 | 554 | def read_integer 555 | top = uint8(:read) 556 | case top 557 | when 0xfd then uint16(:read) 558 | when 0xfe then uint32(:read) 559 | when 0xff then uint64(:read) 560 | else top 561 | end 562 | end 563 | 564 | def write_integer(val) 565 | if val < 0xfd 566 | uint8(:write, val) 567 | elsif val <= 0xffff 568 | uint8(:write, 0xfd) 569 | uint16(:write, val) 570 | elsif val <= 0xffffffff 571 | uint8(:write, 0xfe) 572 | uint32(:write, val) 573 | else 574 | uint8(:write, 0xff) 575 | uint64(:write, val) 576 | end 577 | end 578 | 579 | def integer(rw, val = nil) 580 | case rw 581 | when :read 582 | read_integer 583 | when :write 584 | write_integer(val) 585 | end 586 | end 587 | 588 | def string(rw, val = nil) 589 | case rw 590 | when :read 591 | len = integer(:read) 592 | read_bytes(len) 593 | when :write 594 | integer(:write, val.length) 595 | write_bytes(val) 596 | val 597 | end 598 | end 599 | 600 | def net_addr(rw, val = nil) 601 | # accurate serializing is not necessary 602 | case rw 603 | when :read 604 | read_bytes(26) 605 | nil 606 | when :write 607 | write_bytes([0, '00000000000000000000FFFF', '00000000', 8333].pack('QH*H*n')) 608 | val 609 | end 610 | end 611 | 612 | def relay_flag(rw, val = nil) 613 | case rw 614 | when :read 615 | if @payload.length > 0 616 | uint8(:read) 617 | else 618 | true 619 | end 620 | when :write 621 | unless val 622 | uint8(:write, 0) 623 | end 624 | val 625 | end 626 | end 627 | 628 | def hash256(rw, val = nil) 629 | case rw 630 | when :read 631 | read_bytes(32) 632 | when :write 633 | write_bytes(val) 634 | val 635 | end 636 | end 637 | 638 | def block_hash(rw, val = nil) 639 | case rw 640 | when :read 641 | Key.hash256(@payload[0, 80]) 642 | end 643 | end 644 | 645 | def tx_hash(rw, val = nil) 646 | case rw 647 | when :read 648 | Key.hash256(@payload) 649 | end 650 | end 651 | end 652 | 653 | # 654 | # The blockchain class. It manages and stores Bitcoin blockchain data. 655 | # 656 | class Blockchain 657 | def initialize(keys, data_file_name) 658 | @data_file_name = data_file_name 659 | 660 | keys_hash = Key.hash256(keys.collect { |key, _| key }.sort.join) 661 | 662 | init_data(keys_hash) 663 | load_data 664 | 665 | # new keys are added since the last synchronization 666 | init_data(keys_hash) if @data[:keys_hash] != keys_hash 667 | end 668 | 669 | def init_data(keys_hash) 670 | @data = { blocks: {}, txs: {}, last_height: 0, keys_hash: keys_hash } 671 | end 672 | 673 | def load_data 674 | return unless File.exists?(@data_file_name) 675 | 676 | open(@data_file_name, 'rb') do |file| 677 | @data = Marshal.restore(file) 678 | end 679 | end 680 | 681 | def save_data 682 | open(@data_file_name, 'wb') do |file| 683 | Marshal.dump @data, file 684 | end 685 | end 686 | 687 | def calc_last_hash 688 | # These hashes are genesis blocks' ones. 689 | last_hash = { timestamp: 0, 690 | hash: [IS_TESTNET ? 691 | '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' : 692 | '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'].pack('H*').reverse } 693 | 694 | @data[:blocks].each do |hash, block| 695 | if block[:timestamp] > last_hash[:timestamp] 696 | last_hash = { timestamp: block[:timestamp], hash: hash } 697 | end 698 | end 699 | 700 | last_hash 701 | end 702 | 703 | def blocks 704 | @data[:blocks] 705 | end 706 | 707 | def txs 708 | @data[:txs] 709 | end 710 | 711 | def last_height 712 | @data[:last_height] 713 | end 714 | 715 | def last_height=(v) 716 | @data[:last_height] = v 717 | end 718 | 719 | # 720 | # Get balance for the keys 721 | # 722 | def get_balance(keys) 723 | balance = {} 724 | keys.each do |addr, _| 725 | balance[addr] = 0 726 | end 727 | 728 | set_spent_for_tx_outs! 729 | 730 | @data[:txs].each do |tx_hash, tx| 731 | keys.each do |addr, key| 732 | public_key_hash = key.to_public_key_hash 733 | 734 | tx[:tx_out].each do |tx_out| 735 | # The tx_out was already spent 736 | next if tx_out[:spent] 737 | 738 | if extract_public_key_hash_from_script(tx_out[:pk_script]) == public_key_hash 739 | balance[addr] += tx_out[:value] 740 | end 741 | end 742 | end 743 | end 744 | 745 | balance 746 | end 747 | 748 | # 749 | # This is a heuristic function to find out whether the block is an independent young block. 750 | # An independent block here means a block which have not received one of its ancestors yet. 751 | # We may receive this kind of blocks regardless of getblocks -> inv -> getdata iteration. 752 | # 753 | # To implement it more robustly, you have to construct graph from received blocks, 754 | # do a lot of validations, and actually take the longest block chain. 755 | # 756 | # The reason why the client took this way is simplicity and performance. 757 | # Doing them in Ruby is painful, and also it's not ciritical to explain how Bitcoin client works. 758 | # 759 | def is_young_block(hash) 760 | (@data[:blocks][hash][:timestamp] - Time.now.to_i).abs <= 60 * 60 && !is_too_high(hash) 761 | end 762 | 763 | def accumulate_txs(from_key, amount) 764 | public_key_hash = from_key.to_public_key_hash 765 | 766 | # Refresh spent flags of tx_outs 767 | set_spent_for_tx_outs! 768 | 769 | # In a real SPV client, we should walk along merkle trees to validate the transaction. 770 | # It will be implemented to this client soon. 771 | total_satoshis = 0 772 | tx_in = [] 773 | @data[:txs].each do |tx_hash, tx| 774 | break if total_satoshis >= amount 775 | 776 | matched = nil 777 | pk_script = nil 778 | 779 | tx[:tx_out].each_with_index do |tx_out, index| 780 | next if tx_out[:spent] 781 | 782 | if extract_public_key_hash_from_script(tx_out[:pk_script]) == public_key_hash 783 | total_satoshis += tx_out[:value] 784 | matched = index 785 | pk_script = tx_out[:pk_script] 786 | break 787 | end 788 | end 789 | 790 | if matched 791 | tx_in.push({ previous_output: { :hash => tx[:hash], :index => matched }, 792 | signature_script: '', 793 | sequence: ((1 << 32) - 1), 794 | 795 | # not included in serialized data, but used to make signature 796 | pk_script: pk_script }) 797 | end 798 | end 799 | 800 | { total_satoshis: total_satoshis, tx_in: tx_in } 801 | end 802 | 803 | # 804 | # Set spent flags for all tx_outs. 805 | # If the tx_out is already spent on another transaction's tx_in, it will be set. 806 | # 807 | def set_spent_for_tx_outs! 808 | @data[:txs].each do |tx_hash, tx| 809 | tx[:tx_in].each do |tx_in| 810 | hash = tx_in[:previous_output][:hash] 811 | index = tx_in[:previous_output][:index] 812 | if @data[:txs].has_key?(hash) 813 | @data[:txs][hash][:tx_out][index][:spent] = true 814 | end 815 | end 816 | end 817 | end 818 | 819 | private 820 | 821 | # 822 | # This checks whether the block has previous 5 (= threshold) blocks 823 | # in the received data. 824 | # 825 | def is_too_high(hash) 826 | threshold = 5 827 | cur = 0 828 | while @data[:blocks].has_key?(hash) && cur < threshold 829 | hash = @data[:blocks][hash][:prev_block] 830 | cur += 1 831 | end 832 | cur == threshold 833 | end 834 | 835 | # 836 | # Bitcoin has complex scripting system for its payment, 837 | # but we will only support very basic one. 838 | # 839 | def extract_public_key_hash_from_script(script) 840 | # OP_DUP OP_HASH160 (public key hash) OP_EQUALVERIFY OP_CHECKSIG 841 | unless script[0, 3] == ['76a914'].pack('H*') && 842 | script[23, 2] == ['88ac'].pack('H*') && 843 | script.length == 25 844 | raise 'unsupported script format' 845 | end 846 | 847 | script[3, 20] 848 | end 849 | end 850 | 851 | # 852 | # The network class. It may be split into two or three classes 853 | # to manage multiple connections and features in production. 854 | # 855 | class Network 856 | attr_reader :status, :data 857 | 858 | # 859 | # keys = { name => ECDSA key objects } 860 | # 861 | def initialize(keys, data_file_name) 862 | @message = Message.new 863 | 864 | @keys = keys 865 | 866 | @blockchain = Blockchain.new(@keys, data_file_name) 867 | @last_hash = @blockchain.calc_last_hash 868 | 869 | @is_sync_finished = true 870 | 871 | @requested_data = 0 872 | @received_data = 0 873 | end 874 | 875 | # 876 | # Synchronize the block chain. 877 | # It creates a new thread and returns immediately. 878 | # To know whether the thread was finished, use Network#sync_finished? 879 | # 880 | def sync 881 | Thread.abort_on_exception = true 882 | @is_sync_finished = false 883 | t = Thread.new do 884 | 885 | unless @socket 886 | @status = 'connection establishing ... ' 887 | 888 | @socket = TCPSocket.open(HOST, IS_TESTNET ? 18333 : 8333) 889 | 890 | send_version 891 | end 892 | 893 | if @created_transaction 894 | @status = 'announcing transaction ... ' 895 | 896 | send_transaction_inv 897 | end 898 | 899 | loop do 900 | break if dispatch_message 901 | end 902 | 903 | @is_sync_finished = true 904 | end 905 | t.run 906 | end 907 | 908 | def sync_finished? 909 | @is_sync_finished 910 | end 911 | 912 | # 913 | # Send coins to the address. 914 | # from_key = Key object which the client sends coins from 915 | # to_addr = Receiving address (string) 916 | # transaction_fee = Transaction fee which miners receive 917 | # 918 | def send(from_key, to_addr, amount, transaction_fee = 0) 919 | # The process of announcing a created transaction is as follows: 920 | # Generate tx message and get its hash, and send inv message with the hash to the remote host. 921 | # Then the remote host will send getdata, so you can now actually send tx message. 922 | 923 | to_addr_decoded = Key.decode_base58check(to_addr) 924 | if to_addr_decoded[:type] != :public_key 925 | raise 'invalid address' 926 | end 927 | 928 | accumulated = @blockchain.accumulate_txs(from_key, amount) 929 | 930 | payback = accumulated[:total_satoshis] - amount - transaction_fee 931 | unless payback >= 0 932 | raise "you don't have enough balance to pay" 933 | end 934 | 935 | @created_transaction = sign_transaction(from_key, { 936 | command: :tx, 937 | 938 | version: 1, 939 | 940 | tx_in: accumulated[:tx_in], 941 | tx_out: [{ value: amount, pk_script: generate_pk_script(to_addr_decoded[:data]) }, 942 | { value: payback, pk_script: generate_pk_script(from_key.to_public_key_hash) }], 943 | 944 | lock_time: 0 945 | }) 946 | 947 | @status = '' 948 | end 949 | 950 | # 951 | # Get balance for the keys 952 | # 953 | def get_balance 954 | @blockchain.get_balance(@keys) 955 | end 956 | 957 | 958 | def block(hash) 959 | @blockchain.blocks[hash] 960 | end 961 | 962 | private 963 | 964 | PROTOCOL_VERSION = 70002 965 | 966 | # 967 | # Send version message to the remote host. 968 | # 969 | def send_version 970 | @message.write(@socket, { 971 | command: :version, 972 | 973 | version: PROTOCOL_VERSION, 974 | 975 | # This client should not be asked for full blocks. 976 | services: 0, 977 | 978 | timestamp: Time.now.to_i, 979 | 980 | your_addr: nil, # I found that at least Satoshi client doesn't check it, 981 | my_addr: nil, # so it will be enough for this client. 982 | 983 | nonce: (rand(1 << 64) - 1), # A random number. 984 | 985 | agent: '/bcwallet.rb:1.00/', 986 | height: (@blockchain.blocks.length - 1), # Height of possessed blocks 987 | 988 | # It forces the remote host not to send any 'inv' messages till it receive 'filterload' message. 989 | relay: false 990 | }) 991 | end 992 | 993 | # 994 | # Send filterload message. 995 | # 996 | def send_filterload 997 | hash_funcs = 10 998 | tweak = rand(1 << 32) - 1 999 | 1000 | bf = BloomFilter.new(512, hash_funcs, tweak) 1001 | 1002 | @keys.each do |_, key| 1003 | bf.insert(key.to_public_key) 1004 | bf.insert(key.to_public_key_hash) 1005 | end 1006 | 1007 | @message.write(@socket, { 1008 | command: :filterload, 1009 | 1010 | filter: bf.to_s, 1011 | hash_funcs: hash_funcs, 1012 | tweak: tweak, 1013 | 1014 | # BLOOM_UPDATE_ALL, updates Bloom filter automatically when the client has found matching transactions. 1015 | flag: 1 1016 | }) 1017 | end 1018 | 1019 | def refresh_status 1020 | weight = 50 1021 | perc = (weight * @blockchain.blocks.length / @blockchain.last_height).to_i 1022 | @status = '|' + '=' * perc + '_' * [weight - perc, 0].max + 1023 | "| #{(@blockchain.blocks.length - 1)} / #{@blockchain.last_height} " 1024 | end 1025 | 1026 | # 1027 | # Send getblocks message until it receive all the blocks. 1028 | # If it receives all the blocks, it will return true. Otherwise, it returns false. 1029 | # 1030 | def send_getblocks 1031 | refresh_status 1032 | 1033 | # @blockchain.blocks.length includes block #0 while @blockchain.last_height does not. 1034 | if @blockchain.blocks.length > @blockchain.last_height 1035 | @blockchain.save_data 1036 | return true 1037 | end 1038 | 1039 | if @blockchain.blocks.empty? 1040 | send_getdata([{type: Message::MSG_FILTERED_BLOCK, hash: @last_hash[:hash]}]) 1041 | end 1042 | 1043 | @message.write(@socket, { 1044 | command: :getblocks, 1045 | 1046 | version: PROTOCOL_VERSION, 1047 | block_locator: [@last_hash[:hash]], 1048 | hash_stop: ['00' * 32].pack('H*') 1049 | }) 1050 | 1051 | false 1052 | end 1053 | 1054 | # 1055 | # Send getdata message for the inventory while rewriting MSG_BLOCK to MSG_FILTERED_BLOCK 1056 | # 1057 | def send_getdata(inventory) 1058 | @message.write(@socket, { 1059 | command: :getdata, 1060 | 1061 | inventory: inventory.collect do |elm| 1062 | # receive merkleblock instead of usual block 1063 | {type: (elm[:type] == Message::MSG_BLOCK ? Message::MSG_FILTERED_BLOCK : elm[:type]), 1064 | hash: elm[:hash]} 1065 | end 1066 | }) 1067 | end 1068 | 1069 | # 1070 | # Send inv message when you created a transaction 1071 | # 1072 | def send_transaction_inv 1073 | payload = @message.serialize(@created_transaction) 1074 | 1075 | @created_transaction[:hash] = Key.hash256(payload) 1076 | 1077 | @message.write(@socket, { 1078 | command: :inv, 1079 | inventory: [{type: Message::MSG_TX, hash: @created_transaction[:hash]}] 1080 | }) 1081 | end 1082 | 1083 | # 1084 | # Send transaction message you created 1085 | # 1086 | def send_transaction 1087 | @message.write(@socket, @created_transaction) 1088 | 1089 | @socket.flush 1090 | 1091 | sleep 30 1092 | 1093 | @blockchain.txs[@created_transaction[:hash]] = @created_transaction 1094 | 1095 | @blockchain.save_data 1096 | end 1097 | 1098 | # 1099 | # Dispatch messages. It reads message from the remote host, 1100 | # send proper messages back, and then again wait for a message. 1101 | # 1102 | # Returns true if the whole process has been finished, otherwise false. 1103 | # 1104 | def dispatch_message 1105 | message = @message.read(@socket) 1106 | 1107 | case message[:command] 1108 | when :version then dispatch_version(message) 1109 | when :ping then dispatch_ping(message) 1110 | when :inv then dispatch_inv(message) 1111 | when :merkleblock then dispatch_merkleblock(message) 1112 | when :tx then dispatch_tx(message) 1113 | when :getdata then dispatch_getdata(message) 1114 | end 1115 | end 1116 | 1117 | def dispatch_version(message) 1118 | # This is handshake process: 1119 | 1120 | # Local -- version -> Remote 1121 | # Local <- version -- Remote 1122 | # Local -- verack -> Remote 1123 | # Local <- verack -- Remote 1124 | # Local <- ping -- Remote 1125 | # Local -- pong -> Remote 1126 | 1127 | # You've got the latest block height. 1128 | @blockchain.last_height = message[:height] 1129 | @blockchain.save_data 1130 | 1131 | @message.write(@socket, {command: :verack}) 1132 | 1133 | false 1134 | end 1135 | 1136 | def dispatch_ping(message) 1137 | # Reply with pong 1138 | @message.write(@socket, {command: :pong, nonce: message[:nonce]}) 1139 | 1140 | # Handshake finished, so you can do anything you want. 1141 | 1142 | # Set Bloom filter 1143 | send_filterload 1144 | 1145 | # Tell the remote host to send transactions (inv) it has in its memory pool. 1146 | @message.write(@socket, {command: :mempool}) 1147 | 1148 | # Send getblocks on demand and return true 1149 | send_getblocks 1150 | 1151 | end 1152 | 1153 | def dispatch_inv(message) 1154 | send_getdata message[:inventory] 1155 | 1156 | # Memorize number of requests to check whether the client have received all transactions it required. 1157 | @requested_data += message[:inventory].length 1158 | 1159 | false 1160 | end 1161 | 1162 | def dispatch_merkleblock(message) 1163 | @received_data += 1 1164 | 1165 | @blockchain.blocks[message[:hash]] = message 1166 | 1167 | # Described in Blockchain#is_young_block. 1168 | # It supposes that blocks are sent in its height order. Don't try this in production code. 1169 | unless @blockchain.is_young_block(message[:hash]) 1170 | @last_hash = { timestamp: message[:timestamp], hash: message[:hash] } 1171 | end 1172 | 1173 | @requested_data <= @received_data && send_getblocks 1174 | end 1175 | 1176 | def dispatch_tx(message) 1177 | @received_data += 1 1178 | 1179 | @blockchain.txs[message[:hash]] = message 1180 | 1181 | @requested_data <= @received_data && send_getblocks 1182 | end 1183 | 1184 | def dispatch_getdata(message) 1185 | @status = 'sending transaction data ... ' 1186 | 1187 | # Send the transaction you create 1188 | send_transaction 1189 | 1190 | true 1191 | end 1192 | 1193 | def sign_transaction(from_key, transaction) 1194 | # We have generated all data without signatures, so we're now going to generate signatures. 1195 | # However, it is very complicated one. 1196 | signatures = [] 1197 | 1198 | transaction[:tx_in].length.times do |i| 1199 | signatures.push(sign_transaction_of_idx(i)) 1200 | end 1201 | 1202 | signatures.each_with_index do |signature, i| 1203 | transaction[:tx_in][i][:signature_script] = generate_signature_script(signature, from_key) 1204 | end 1205 | 1206 | return transaction 1207 | end 1208 | 1209 | def sign_transaction_of_idx(from_key, transaction, i) 1210 | duplicated = transaction.dup 1211 | duplicated[:tx_in] = duplicated[:tx_in].dup 1212 | duplicated[:tx_in][i] = duplicated[:tx_in][i].dup 1213 | 1214 | # To generate signature, you need hash256 of the whole transaction in special form. 1215 | # The transaction in that form is different in a way that the signature_script 1216 | # field in the tx_in to sign is replaced with pk_script in previous tx_out, 1217 | # and other tx_ins' signature_scripts are empty. 1218 | # (make sure that var_int for the length is also set to zero) 1219 | # 1220 | # For further information, see: 1221 | # https://en.bitcoin.it/w/images/en/7/70/Bitcoin_OpCheckSig_InDetail.png 1222 | # 1223 | 1224 | duplicated[:tx_in][i][:signature_script] = transaction[:tx_in][i][:pk_script] 1225 | 1226 | payload = @message.serialize(duplicated) 1227 | 1228 | # hash256 includes type code field (see the figure in the URL above) 1229 | verified_str = Key.hash256(payload + [1].pack('V')) 1230 | 1231 | from_key.sign(verified_str) 1232 | end 1233 | 1234 | def generate_pk_script(public_key_hash) 1235 | # pk_script field is constructed in Bitcoin's scripting system 1236 | # https://en.bitcoin.it/wiki/Script 1237 | # 1238 | prefix = ['76a914'].pack('H*') # OP_DUP OP_HASH160 [length of the address] 1239 | postfix = ['88ac'].pack('H*') # OP_EQUALVERIFY OP_CHECKSIG 1240 | 1241 | prefix + public_key_hash + postfix 1242 | end 1243 | 1244 | def generate_signature_script(signature, from_key) 1245 | # see the figure in the 1246 | # https://en.bitcoin.it/w/images/en/7/70/Bitcoin_OpCheckSig_InDetail.png 1247 | 1248 | public_key = from_key.to_public_key 1249 | 1250 | script = [signature.length + 1].pack('C') + signature + [1].pack('C') 1251 | script += [public_key.length].pack('C') + public_key 1252 | 1253 | script 1254 | end 1255 | end 1256 | 1257 | 1258 | # 1259 | # The class deals with command line arguments and the key file. 1260 | # 1261 | class BCWallet 1262 | def initialize(argv, keys_file_name, data_file_name) 1263 | @argv = argv 1264 | @keys_file_name = keys_file_name 1265 | @data_file_name = data_file_name 1266 | @network = nil 1267 | end 1268 | 1269 | def run 1270 | return usage if @argv.length < 1 1271 | 1272 | # check argument numbers 1273 | case @argv.first 1274 | when 'generate' then return if require_args(1) 1275 | when 'list' then return if require_args(0) 1276 | when 'export' then return if require_args(1) 1277 | when 'balance' then return if require_args(0) 1278 | when 'send' then return if require_args(3) 1279 | when 'block' then return if require_args(1) 1280 | else 1281 | usage 'invalid command' 1282 | return 1283 | end 1284 | 1285 | load_keys 1286 | 1287 | case @argv.first 1288 | when 'generate' then generate(@argv[1]) # name 1289 | when 'list' then list 1290 | when 'export' then export(@argv[1]) # name 1291 | when 'balance' then balance 1292 | when 'send' then send(@argv[1], @argv[2], btc_to_satoshi(@argv[3].to_r)) # name, to, amount 1293 | when 'block' then block(@argv[1]) # hash 1294 | end 1295 | end 1296 | 1297 | private 1298 | 1299 | def usage(error = nil) 1300 | warn "bcwallet.rb: #{error}\n\n" if error 1301 | warn "bcwallet.rb: Educational Bitcoin Client" 1302 | warn "Usage: ruby bcwallet.rb []" 1303 | warn "commands:" 1304 | warn " generate \t\tgenerate a new Bitcoin address" 1305 | warn " list\t\t\tshow list for all Bitcoin addresses" 1306 | warn " export \t\tshow private key for the Bitcoin address" 1307 | warn " balance\t\t\tshow balances for all Bitcoin addresses" 1308 | warn " send \ttransfer coins to the Bitcoin address" 1309 | end 1310 | 1311 | def require_args(number) 1312 | if @argv.length < number + 1 1313 | usage 'missing arguments' 1314 | true 1315 | else 1316 | false 1317 | end 1318 | end 1319 | 1320 | def load_keys 1321 | @keys = {} 1322 | 1323 | return unless File.exists?(@keys_file_name) 1324 | 1325 | open(@keys_file_name, 'r') do |file| 1326 | file.read.lines.each do |line| 1327 | name, der = line.split(' ') 1328 | @keys[name] = Key.new(der) 1329 | end 1330 | end 1331 | end 1332 | 1333 | def init_network 1334 | @network = Network.new(@keys, @data_file_name) 1335 | end 1336 | 1337 | def wait_for_sync(mode = nil) 1338 | @network.sync 1339 | 1340 | rotate = ['/', '-', '\\', '|'] 1341 | cur = 0 1342 | 1343 | while !@network.sync_finished? 1344 | $stderr.print "#{@network.status}#{rotate[cur]}\r" 1345 | 1346 | cur = (cur + 1) % rotate.length 1347 | 1348 | sleep 0.1 1349 | end 1350 | 1351 | $stderr.print "#{@network.status}done.\n" 1352 | if mode == :tx 1353 | $stderr.print "Transaction sent.\n\n" 1354 | else 1355 | $stderr.print "Block chain synchronized.\n\n" 1356 | end 1357 | end 1358 | 1359 | def btc_to_satoshi(btc) 1360 | btc * Rational(10 ** 8) 1361 | end 1362 | 1363 | def satoshi_to_btc(satoshi) 1364 | Rational(satoshi, 10**8) 1365 | end 1366 | 1367 | def generate(name) 1368 | return usage "the name \"#{name}\" already exists" if @keys.has_key?(name) 1369 | 1370 | key = Key.new 1371 | 1372 | open(@keys_file_name, 'a') do |file| 1373 | file.write "#{name} #{key.to_der_hex_s}\n" 1374 | end 1375 | 1376 | puts "new Bitcoin address \"#{name}\" generated: #{key.to_address_s}" 1377 | end 1378 | 1379 | def list 1380 | if @keys.empty? 1381 | puts 'No addresses available' 1382 | return 1383 | end 1384 | 1385 | puts 'List of available Bitcoin addresses: ' 1386 | @keys.each do |name, key| 1387 | puts " #{name}: #{key.to_address_s}" 1388 | end 1389 | end 1390 | 1391 | def export(name) 1392 | return usage "an address named #{name} doesn't exist" unless @keys.has_key?(name) 1393 | 1394 | $stderr.print "Are you sure you want to export private key for \"#{name}\"? (yes/NO): " 1395 | 1396 | if $stdin.gets.chomp.downcase == 'yes' 1397 | puts @keys[name].to_private_key_s 1398 | end 1399 | end 1400 | 1401 | def balance 1402 | $stderr.print "loading data ...\r" 1403 | 1404 | init_network 1405 | wait_for_sync 1406 | 1407 | puts 'Balances for available Bitcoin addresses: ' 1408 | 1409 | balance = @network.get_balance 1410 | balance.each do |addr, satoshi| 1411 | puts " #{ addr }: #{ sprintf('%.8f', satoshi_to_btc(satoshi)) } BTC" 1412 | end 1413 | end 1414 | 1415 | def confirm_send(name, to, amount) 1416 | $stderr.print "Are you sure you want to send\n" 1417 | $stderr.print " #{sprintf('%.8f', satoshi_to_btc(amount))} BTC\n" 1418 | $stderr.print "from\n \"#{name}\"\nto\n \"#{to}\"\n? (yes/no): " 1419 | 1420 | $stdin.gets.chomp.downcase == 'yes' 1421 | end 1422 | 1423 | def send(name, to, amount) 1424 | return usage "an address named #{name} doesn't exist" unless @keys.has_key?(name) 1425 | 1426 | init_network 1427 | wait_for_sync 1428 | 1429 | if confirm_send(name, to, amount) 1430 | begin 1431 | @network.send(@keys[name], to, amount) 1432 | rescue => e 1433 | warn "bcwallet.rb: #{e}" 1434 | return 1435 | end 1436 | wait_for_sync 1437 | end 1438 | end 1439 | 1440 | def block(hash) 1441 | init_network 1442 | puts @network.block([hash].pack('H*').reverse) 1443 | end 1444 | end 1445 | 1446 | if caller.length == 0 1447 | unless IS_TESTNET 1448 | warn 'WARNING: RUNNING UNDER MAIN NETWORK MODE' 1449 | end 1450 | 1451 | key_file_name = IS_TESTNET ? 'keys_testnet' : 'keys' 1452 | data_file_name = IS_TESTNET ? 'data_testnet' : 'data' 1453 | 1454 | bcwallet = BCWallet.new(ARGV, key_file_name, data_file_name) 1455 | 1456 | bcwallet.run 1457 | end 1458 | 1459 | -------------------------------------------------------------------------------- /test_bcwallet.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ENV['CI'] then 4 | require 'coveralls' 5 | Coveralls.wear! 6 | end 7 | 8 | require 'tmpdir' 9 | require 'timeout' 10 | require 'minitest/autorun' 11 | require './bcwallet' 12 | 13 | class TestKey < MiniTest::Test 14 | def test_base58_encode 15 | assert_equal '2cFupjhnEsSn59qHXstmK2ffpLv2', 16 | Key.encode_base58(['73696d706c792061206c6f6e6720737472696e67'].pack('H*')) 17 | end 18 | 19 | def test_base58_decode 20 | assert_equal ['73696d706c792061206c6f6e6720737472696e67'].pack('H*'), 21 | Key.decode_base58('2cFupjhnEsSn59qHXstmK2ffpLv2') 22 | end 23 | 24 | def test_base58_encode_decode 25 | assert_equal 'foobarbazhoge', Key.decode_base58(Key.encode_base58('foobarbazhoge')) 26 | end 27 | 28 | def test_base58_encode_decode_when_begin_with_00 29 | assert_equal [0x00, 0x01, 0x02], Key.decode_base58(Key.encode_base58([0x00, 0x01, 0x02].pack('C*'))).unpack('C*') 30 | end 31 | 32 | def test_key_generation 33 | key = Key.new 34 | 35 | address_str = key.to_address_s 36 | private_key_str = key.to_private_key_s 37 | 38 | assert_match /[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+/, address_str 39 | assert_match /[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+/, private_key_str 40 | end 41 | end 42 | 43 | class TestBloomFilter < MiniTest::Test 44 | def test_murmur_hash 45 | bf = BloomFilter.new(1, 1, 1) 46 | assert_equal 0x2a2884ba, bf.hash(0xabcdef, "hogehoge") 47 | assert_equal 0xcdcbf1ad, bf.hash(0xabcdef, "foobarbaz") 48 | assert_equal 0xc28e9cab, bf.hash(0xabcdef, "abcdefghijklmnopqrstuvwxyz") 49 | assert_equal 0xfe1d612e, bf.hash(0xabcdef, "qwertyuiop") 50 | end 51 | end 52 | 53 | class TestMessage < MiniTest::Test 54 | def test_version_message_serialize 55 | m = Message.new 56 | b = m.serialize({ 57 | command: :version, 58 | version: 31900, 59 | services: 1, 60 | timestamp: 1292899814, 61 | your_addr: nil, 62 | my_addr: nil, 63 | nonce: 1393780771635895773, 64 | agent: '', 65 | height: 98645, 66 | relay: true 67 | }) 68 | 69 | assert_equal(b.unpack('C*'), 70 | [0x9C, 0x7C, 0x00, 0x00, 71 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0xE6, 0x15, 0x10, 0x4D, 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8D, 74 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8D, 75 | 0xDD, 0x9D, 0x20, 0x2C, 0x3A, 0xB4, 0x57, 0x13, 76 | 0x00, 77 | 0x55, 0x81, 0x01, 0x00]) 78 | end 79 | 80 | def test_version_message_deserialize 81 | b = [0x9C, 0x7C, 0x00, 0x00, 82 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0xE6, 0x15, 0x10, 0x4D, 0x00, 0x00, 0x00, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8D, 85 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8D, 86 | 0xDD, 0x9D, 0x20, 0x2C, 0x3A, 0xB4, 0x57, 0x13, 87 | 0x00, 88 | 0x55, 0x81, 0x01, 0x00] 89 | 90 | m = Message.new 91 | msg = m.deserialize(:version, b.pack('C*')) 92 | 93 | assert_equal :version, msg[:command] 94 | assert_equal 31900, msg[:version] 95 | assert_equal 1, msg[:services] 96 | assert_equal 1292899814, msg[:timestamp] 97 | assert_equal nil, msg[:your_addr] 98 | assert_equal nil, msg[:my_addr] 99 | assert_equal 1393780771635895773, msg[:nonce] 100 | assert_equal '', msg[:agent] 101 | assert_equal 98645, msg[:height] 102 | assert_equal true, msg[:relay] 103 | end 104 | 105 | def test_inv_message_serialize 106 | m = Message.new 107 | b = m.serialize({ 108 | command: :inv, 109 | inventory: [ 110 | { type: Message::MSG_TX, 111 | hash: [ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 ].pack('C*') }, 113 | { type: Message::MSG_BLOCK, 114 | hash: [ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 ].pack('C*') } 116 | ] 117 | }) 118 | 119 | assert_equal b.unpack('C*'), [ 120 | 0x02, 121 | 0x01, 0x00, 0x00, 0x00, 122 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 124 | 0x02, 0x00, 0x00, 0x00, 125 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 126 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 127 | ] 128 | end 129 | 130 | def test_inv_message_deserialize 131 | b = [ 132 | 0x02, 133 | 0x01, 0x00, 0x00, 0x00, 134 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 135 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 136 | 0x02, 0x00, 0x00, 0x00, 137 | 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 138 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 139 | ].pack('C*') 140 | 141 | m = Message.new 142 | msg = m.deserialize(:inv, b) 143 | 144 | assert_equal msg[:command], :inv 145 | assert_equal msg[:inventory].length, 2 146 | assert_equal msg[:inventory][0][:type], Message::MSG_TX 147 | assert_equal(msg[:inventory][0][:hash], 148 | [ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 149 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 ].pack('C*')) 150 | assert_equal msg[:inventory][1][:type], Message::MSG_BLOCK 151 | assert_equal(msg[:inventory][1][:hash], 152 | [ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 153 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04 ].pack('C*')) 154 | end 155 | end 156 | 157 | class TestBCWallet < MiniTest::Test 158 | HANDSHAKES = [ 159 | # version 160 | 0x0b, 0x11, 0x09, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 161 | 0x66, 0x00, 0x00, 0x00, 0x11, 0x20, 0xd9, 0xde, 0x72, 0x11, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 162 | 0x00, 0x00, 0x00, 0x00, 0x2f, 0x76, 0x41, 0x56, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 163 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 164 | 0x77, 0x48, 0xc0, 0x53, 0xdb, 0xe8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 165 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x47, 0x9d, 166 | 0xc7, 0x8a, 0x2c, 0xb2, 0xd1, 0x17, 0x63, 0x4f, 0x10, 0x2f, 0x53, 0x61, 0x74, 0x6f, 0x73, 0x68, 167 | 0x69, 0x3a, 0x30, 0x2e, 0x31, 0x31, 0x2e, 0x30, 0x2f, 0x01, 0x00, 0x00, 0x00, 0x01, 168 | 169 | # verack 170 | 0x0b, 0x11, 0x09, 0x07, 0x76, 0x65, 0x72, 0x61, 0x63, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 171 | 0x00, 0x00, 0x00, 0x00, 0x5d, 0xf6, 0xe0, 0xe2, 172 | 173 | # ping 174 | 0x0b, 0x11, 0x09, 0x07, 0x70, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 175 | 0x08, 0x00, 0x00, 0x00, 0x3b, 0x7b, 0xb8, 0xb4, 0x71, 0x44, 0x02, 0x13, 0x0d, 0xa2, 0xac, 0xee ] 176 | 177 | BLOCKS = [ 178 | # genesis block (merkleblock) 179 | 0x0b, 0x11, 0x09, 0x07, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 180 | 0x77, 0x00, 0x00, 0x00, 0xd7, 0xba, 0xe3, 0xf8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 181 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 182 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0xa3, 0xed, 0xfd, 183 | 0x7a, 0x7b, 0x12, 0xb2, 0x7a, 0xc7, 0x2c, 0x3e, 0x67, 0x76, 0x8f, 0x61, 0x7f, 0xc8, 0x1b, 0xc3, 184 | 0x88, 0x8a, 0x51, 0x32, 0x3a, 0x9f, 0xb8, 0xaa, 0x4b, 0x1e, 0x5e, 0x4a, 0xda, 0xe5, 0x49, 0x4d, 185 | 0xff, 0xff, 0x00, 0x1d, 0x1a, 0xa4, 0xae, 0x18, 0x01, 0x00, 0x00, 0x00, 0x01, 0x3b, 0xa3, 0xed, 186 | 0xfd, 0x7a, 0x7b, 0x12, 0xb2, 0x7a, 0xc7, 0x2c, 0x3e, 0x67, 0x76, 0x8f, 0x61, 0x7f, 0xc8, 0x1b, 187 | 0xc3, 0x88, 0x8a, 0x51, 0x32, 0x3a, 0x9f, 0xb8, 0xaa, 0x4b, 0x1e, 0x5e, 0x4a, 0x01, 0x00, 188 | 189 | # #1 block (merkleblock) 190 | 0x0b, 0x11, 0x09, 0x07, 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x00, 191 | 0x77, 0x00, 0x00, 0x00, 0x6c, 0xd4, 0xd0, 0x39, 0x01, 0x00, 0x00, 0x00, 0x43, 0x49, 0x7f, 0xd7, 192 | 0xf8, 0x26, 0x95, 0x71, 0x08, 0xf4, 0xa3, 0x0f, 0xd9, 0xce, 0xc3, 0xae, 0xba, 0x79, 0x97, 0x20, 193 | 0x84, 0xe9, 0x0e, 0xad, 0x01, 0xea, 0x33, 0x09, 0x00, 0x00, 0x00, 0x00, 0xba, 0xc8, 0xb0, 0xfa, 194 | 0x92, 0x7c, 0x0a, 0xc8, 0x23, 0x42, 0x87, 0xe3, 0x3c, 0x5f, 0x74, 0xd3, 0x8d, 0x35, 0x48, 0x20, 195 | 0xe2, 0x47, 0x56, 0xad, 0x70, 0x9d, 0x70, 0x38, 0xfc, 0x5f, 0x31, 0xf0, 0x20, 0xe7, 0x49, 0x4d, 196 | 0xff, 0xff, 0x00, 0x1d, 0x03, 0xe4, 0xb6, 0x72, 0x01, 0x00, 0x00, 0x00, 0x01, 0xba, 0xc8, 0xb0, 197 | 0xfa, 0x92, 0x7c, 0x0a, 0xc8, 0x23, 0x42, 0x87, 0xe3, 0x3c, 0x5f, 0x74, 0xd3, 0x8d, 0x35, 0x48, 198 | 0x20, 0xe2, 0x47, 0x56, 0xad, 0x70, 0x9d, 0x70, 0x38, 0xfc, 0x5f, 0x31, 0xf0, 0x01, 0x00 ] 199 | 200 | def test_invalid_arguments 201 | Dir.mktmpdir do |dir| 202 | key_file_name = "#{dir}/keys" 203 | data_file_name = "#{dir}/data" 204 | 205 | assert_output nil, /Usage\: ruby bcwallet\.rb/ do 206 | BCWallet.new([''], key_file_name, data_file_name).run 207 | end 208 | 209 | assert_output nil, /bcwallet\.rb: invalid command/ do 210 | BCWallet.new(['foo'], key_file_name, data_file_name).run 211 | end 212 | 213 | assert_output nil, /bcwallet.rb: missing arguments/ do 214 | BCWallet.new(['export'], key_file_name, data_file_name).run 215 | end 216 | 217 | assert_output nil, /bcwallet\.rb: an address named foo doesn't exist/ do 218 | BCWallet.new( 219 | ['send', 'foo', 'n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi', '1.00'], 220 | key_file_name, data_file_name).run 221 | end 222 | end 223 | end 224 | 225 | def test_generate_list 226 | Dir.mktmpdir do |dir| 227 | key_file_name = "#{dir}/keys" 228 | data_file_name = "#{dir}/data" 229 | 230 | assert_output /No addresses available/, nil do 231 | BCWallet.new(['list'], key_file_name, data_file_name).run 232 | end 233 | 234 | assert_output /new Bitcoin address "peryaudo" generated/ do 235 | BCWallet.new(['generate', 'peryaudo'], key_file_name, data_file_name).run 236 | end 237 | 238 | assert_output nil, /the name "peryaudo" already exists/ do 239 | BCWallet.new(['generate', 'peryaudo'], key_file_name, data_file_name).run 240 | end 241 | 242 | assert_output /peryaudo/ do 243 | BCWallet.new(['list'], key_file_name, data_file_name).run 244 | end 245 | end 246 | end 247 | 248 | def test_export 249 | Dir.mktmpdir do |dir| 250 | key_file_name = "#{dir}/keys" 251 | data_file_name = "#{dir}/data" 252 | 253 | assert_output /new Bitcoin address "peryaudo" generated/ do 254 | BCWallet.new(['generate', 'peryaudo'], key_file_name, data_file_name).run 255 | end 256 | 257 | $stdin = StringIO.new('yes', 'r') 258 | 259 | assert_output nil, /Are you sure you want to export private key for "peryaudo"/ do 260 | BCWallet.new(['export', 'peryaudo'], key_file_name, data_file_name).run 261 | end 262 | end 263 | end 264 | 265 | def test_balance 266 | Dir.mktmpdir do |dir| 267 | key_file_name = "#{dir}/keys" 268 | data_file_name = "#{dir}/data" 269 | 270 | assert_output /new Bitcoin address "peryaudo" generated/ do 271 | BCWallet.new(['generate', 'peryaudo'], key_file_name, data_file_name).run 272 | end 273 | 274 | stream = StringIO.new((HANDSHAKES + BLOCKS).pack('C*')) 275 | def stream.write(str) 276 | str.length 277 | end 278 | 279 | TCPSocket.stub :open, stream do 280 | timeout 10 do 281 | assert_output /peryaudo: 0\.00000000 BTC/, nil do 282 | BCWallet.new(['balance'], key_file_name, data_file_name).run 283 | end 284 | end 285 | 286 | end 287 | 288 | assert_output /merkleblock/, nil do 289 | BCWallet.new( 290 | ['block', '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943'], 291 | key_file_name, data_file_name).run 292 | end 293 | end 294 | end 295 | 296 | def test_send 297 | # TODO(peryaudo): do real checks to ensure sending functionality is working 298 | 299 | Dir.mktmpdir do |dir| 300 | key_file_name = "#{dir}/keys" 301 | data_file_name = "#{dir}/data" 302 | 303 | assert_output /new Bitcoin address "peryaudo" generated/ do 304 | BCWallet.new(['generate', 'peryaudo'], key_file_name, data_file_name).run 305 | end 306 | 307 | $stream = StringIO.new((HANDSHAKES + BLOCKS).pack('C*')) 308 | def $stream.write(str) 309 | str.length 310 | end 311 | 312 | $stdin = StringIO.new 313 | def $stdin.gets 314 | $stream = StringIO.new(HANDSHAKES.pack('C*')) 315 | return 'yes' 316 | end 317 | 318 | TCPSocket.stub :open, $stream do 319 | timeout 10 do 320 | assert_output nil, /you don't have enough balance to pay/ do 321 | BCWallet.new( 322 | ['send', 'peryaudo', 'n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi', '1.00'], 323 | key_file_name, data_file_name).run 324 | end 325 | end 326 | end 327 | 328 | $stream = StringIO.new((HANDSHAKES + BLOCKS).pack('C*')) 329 | def $stream.write(str) 330 | str.length 331 | end 332 | 333 | TCPSocket.stub :open, $stream do 334 | timeout 10 do 335 | assert_output nil, nil do 336 | BCWallet.new( 337 | ['send', 'peryaudo', 'n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi', '0.00'], 338 | key_file_name, data_file_name).run 339 | end 340 | end 341 | end 342 | end 343 | end 344 | end 345 | --------------------------------------------------------------------------------