Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ Lint/RaiseException:
Lint/StructNewOverride:
Description: Disallow overriding the `Struct` built-in methods via `Struct.new`.
Enabled: true
Lint/MissingSuper:
Exclude:
- 'lib/bitcoin/network/messages/*.rb'
Rails/Delegate:
Description: Prefer delegate method for delegations.
Enabled: false
Expand Down
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ source "https://rubygems.org"

# gem "rails"

gem "rspec", "~> 3.11", :groups => [:development, :test]
gem "pry", :groups => [:development]
gem 'pry-byebug', :groups => [:development]
gem "rspec", "~> 3.11", :groups => [:development, :test]
gem "timecop", "~> 0.9.5", groups: [:development, :test]
11 changes: 9 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
byebug (11.1.3)
coderay (1.1.3)
diff-lcs (1.5.0)
method_source (1.0.0)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
Expand All @@ -20,15 +24,18 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-support (3.11.0)
timecop (0.9.5)

PLATFORMS
arm64-darwin-21
x86_64-linux
ruby
x86_64-linux

DEPENDENCIES
pry
pry-byebug
rspec (~> 3.11)
timecop (~> 0.9.5)

BUNDLED WITH
2.3.16
2.3.20
17 changes: 15 additions & 2 deletions lib/bitcoin/block.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
require_relative '../bitcoin_data_io'
require_relative '../encoding_helper'
require_relative '../hash_helper'
require_relative '../merkle_helper'

module Bitcoin
class Block
include EncodingHelper
extend EncodingHelper

GENESIS_BLOCK = from_hex_to_bytes('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c')
TESTNET_GENESIS_BLOCK = from_hex_to_bytes('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18')
LOWEST_BITS = from_hex_to_bytes('ffff001d')

MAX_TARGET = 0xffff * 256**(0x1d - 3)
TWO_WEEKS = 60 * 60 * 24 * 14

def initialize(version, prev_block, merkle_root, timestamp, bits, nonce)
def initialize(version, prev_block, merkle_root, timestamp, bits, nonce, tx_hashes: nil)
@version = version
@prev_block = prev_block
@merkle_root = merkle_root
@timestamp = timestamp
@bits = bits
@nonce = nonce
@tx_hashes = tx_hashes
end

attr_accessor :version, :prev_block, :merkle_root, :timestamp, :bits, :nonce
attr_accessor :version, :prev_block, :merkle_root, :timestamp, :bits, :nonce, :tx_hashes

def self.parse(io)
io = BitcoinDataIO(io)
Expand Down Expand Up @@ -103,5 +109,12 @@ def pow_valid?
block_header_hash = HashHelper.hash256(serialize)
little_endian_to_int(block_header_hash) < target
end

def merkle_root_valid?
hashes = @tx_hashes.map(&:reverse)
computed_root = MerkleHelper.merkle_root(hashes)

computed_root.reverse == @merkle_root
end
end
end
55 changes: 55 additions & 0 deletions lib/bitcoin/merkle_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require_relative '../bitcoin_data_io'
require_relative '../encoding_helper'
require_relative '../merkle_tree'

module Bitcoin
class MerkleBlock
include EncodingHelper
extend EncodingHelper

def initialize(version, prev_block, merkle_root, timestamp, bits, nonce,
total_tx, tx_hashes, flags)

@version = version
@prev_block = prev_block
@merkle_root = merkle_root
@timestamp = timestamp
@bits = bits
@nonce = nonce
@total_tx = total_tx
@tx_hashes = tx_hashes
@flags = flags
end

attr_accessor :version, :prev_block, :merkle_root, :timestamp, :bits, :nonce,
:total_tx, :tx_hashes, :flags

def self.parse(io)
io = BitcoinDataIO(io)

version = little_endian_to_int(io.read(4))
prev_block = io.read_le(32)
merkle_root = io.read_le(32)
timestamp = little_endian_to_int(io.read(4))
bits = io.read(4)
nonce = io.read(4)
total_tx = little_endian_to_int(io.read(4))
num_hashes = io.read_varint
tx_hashes = []
num_hashes.times { tx_hashes << io.read_le(32) }
num_flags = io.read_varint
flags = io.read_le(num_flags)

new(version, prev_block, merkle_root, timestamp, bits, nonce, total_tx, tx_hashes, flags)
end

def valid?
flag_bits = bytes_to_bit_field(@flags.reverse)
hashes = @tx_hashes.map(&:reverse)
tree = MerkleTree.new(@total_tx)
tree.populate_tree(flag_bits, hashes)

tree.root.reverse == @merkle_root
end
end
end
74 changes: 74 additions & 0 deletions lib/bitcoin/network/envelope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# encoding: ascii-8bit

require_relative '../../bitcoin_data_io'
require_relative '../../encoding_helper'
require_relative '../../hash_helper'

module Bitcoin
module Network
class Envelope
include EncodingHelper
extend EncodingHelper

NETWORK_MAGIC = "f9beb4d9"
TESTNET_NETWORK_MAGIC = "0b110907"

def initialize(command, payload, testnet=false)
@command_bytes = command
@payload_bytes = payload
@magic_hex = testnet ? TESTNET_NETWORK_MAGIC : NETWORK_MAGIC
end

attr_accessor :command_bytes, :payload_bytes, :magic_hex

def self.parse(io)
bitcoin_io = BitcoinDataIO(io)
network_magic_bytes = bitcoin_io.read(4)
raise IOError.new('no data received') if network_magic_bytes.nil?

network_magic = bytes_to_hex(network_magic_bytes)
raise IOError.new('unrecognized network magic') unless [
NETWORK_MAGIC, TESTNET_NETWORK_MAGIC
].include? network_magic

command = bitcoin_io.read(12).delete("\x00")
payload_length = bitcoin_io.read_le_int32
checksum = bitcoin_io.read(4)
payload = bitcoin_io.read(payload_length) || ''

raise IOError.new("checksum doesn't match") unless checksum_match?(payload, checksum)

new(command, payload, network_magic == TESTNET_NETWORK_MAGIC)
end

def self.checksum(payload)
HashHelper.hash256(payload).slice(0, 4)
end

def self.checksum_match?(payload, _checksum)
checksum(payload) == _checksum
end

def to_s
"#{@command_bytes}: #{@payload_bytes}"
end

def serialize
result = from_hex_to_bytes(@magic_hex)
result << @command_bytes
result << "\x00" * (12 - @command_bytes.length)
result << int_to_little_endian(@payload_bytes.length, 4)
result << self.class.checksum(@payload_bytes)
result << @payload_bytes

result
end

def stream
StringIO.new(@payload_bytes)
end

private_class_method :checksum_match?
end
end
end
16 changes: 16 additions & 0 deletions lib/bitcoin/network/messages/base_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# encoding: ascii-8bit
require_relative '../../../encoding_helper'

module Bitcoin
module Network
module Messages
class BaseMessage
include EncodingHelper

def command
self.class::COMMAND
end
end
end
end
end
31 changes: 31 additions & 0 deletions lib/bitcoin/network/messages/get_headers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# encoding: ascii-8bit
require_relative './base_message'

module Bitcoin
module Network
module Messages
class GetHeaders < BaseMessage
COMMAND = "getheaders"

def initialize(version: 70015, num_hashes: 1, start_block: nil, end_block: nil)
@version = version
@num_hashes = num_hashes

raise 'a start block is required' if start_block.nil?

@start_block = start_block
@end_block = end_block.nil? ? "\x00" * 32 : end_block
end

def serialize
result = int_to_little_endian(@version, 4)
result << encode_varint(@num_hashes)
result << @start_block.reverse
result << @end_block.reverse

result
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/bitcoin/network/messages/headers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# encoding: ascii-8bit
require_relative '../../../bitcoin_data_io'
require_relative './base_message'

module Bitcoin
module Network
module Messages
class Headers < BaseMessage
COMMAND = "headers"

def initialize(blocks:)
@blocks = blocks
end

attr_accessor :blocks

def self.parse(_io)
io = BitcoinDataIO(_io)

num_headers = io.read_varint
blocks = []

num_headers.times do
blocks << Bitcoin::Block.parse(io)
num_txs = io.read_varint

raise RuntimeError('number of txs not 0') unless num_txs.zero?
end

new(blocks: blocks)
end
end
end
end
end
25 changes: 25 additions & 0 deletions lib/bitcoin/network/messages/ping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# encoding: ascii-8bit
require_relative './base_message'

module Bitcoin
module Network
module Messages
class Ping < BaseMessage
COMMAND = "ping"

def initialize(nonce)
@nonce = nonce
end

def serialize
@nonce
end

def self.parse(stream)
nonce = stream.read(8)
new(nonce)
end
end
end
end
end
25 changes: 25 additions & 0 deletions lib/bitcoin/network/messages/pong.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# encoding: ascii-8bit
require_relative './base_message'

module Bitcoin
module Network
module Messages
class Pong < BaseMessage
COMMAND = "pong"

def initialize(nonce)
@nonce = nonce
end

def serialize
@nonce
end

def self.parse(stream)
nonce = stream.read(8)
new(nonce)
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/bitcoin/network/messages/verack.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# encoding: ascii-8bit
require_relative './base_message'

module Bitcoin
module Network
module Messages
class Verack < BaseMessage
COMMAND = "verack"

def serialize
''
end

def self.parse(_)
new
end
end
end
end
end
Loading