From ae4bbf0b8ae573aafaba26d0536ec58c686081ed Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Fri, 28 Oct 2022 01:57:18 -0300 Subject: [PATCH 01/17] chore: add Timecop Co-authored-by: Maurice Poirrier --- Gemfile | 5 +++-- Gemfile.lock | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index a35c28a..c706e16 100644 --- a/Gemfile +++ b/Gemfile @@ -4,5 +4,6 @@ source "https://rubygems.org" # gem "rails" -gem "rspec", "~> 3.11", :groups => [:development, :test] -gem "pry", :groups => [:development] +gem "pry", groups: [:development] +gem "rspec", "~> 3.11", groups: [:development, :test] +gem "timecop", "~> 0.9.5", groups: [:development, :test] diff --git a/Gemfile.lock b/Gemfile.lock index fb60eec..54f107a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,15 +20,17 @@ 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 rspec (~> 3.11) + timecop (~> 0.9.5) BUNDLED WITH 2.3.16 From e440e074f09b547d9b0cb768412f745daa9496a0 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Fri, 28 Oct 2022 02:00:42 -0300 Subject: [PATCH 02/17] feat(encoding-helper): add bytes_to_hex and int_to_big_endian Co-authored-by: Maurice Poirrier --- lib/encoding_helper.rb | 8 ++++++++ spec/encoding_helper_spec.rb | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/encoding_helper.rb b/lib/encoding_helper.rb index 63b5a0c..1ca63ee 100644 --- a/lib/encoding_helper.rb +++ b/lib/encoding_helper.rb @@ -15,6 +15,10 @@ def from_hex_to_bytes(hex) [hex.strip].pack("H*") end + def bytes_to_hex(bytes) + bytes.unpack1("H*") + end + def to_bytes(integer, bytes, endianness) byte_array = [0] * bytes integer.digits(256).each_with_index do |byte, index| @@ -61,6 +65,10 @@ def int_to_little_endian(int, length) to_bytes(int, length, 'little') end + def int_to_big_endian(int, length) + to_bytes(int, length, 'big') + end + def encode_varint(integer) if integer < 0xfd int_to_little_endian(integer, 1) diff --git a/spec/encoding_helper_spec.rb b/spec/encoding_helper_spec.rb index 115fdfc..809059b 100644 --- a/spec/encoding_helper_spec.rb +++ b/spec/encoding_helper_spec.rb @@ -74,6 +74,12 @@ end end + describe '#int_to_big_endian' do + it 'takes an integer and returns the big-endian byte sequence of length' do + expect(described_module.int_to_big_endian(1, 4)).to eq "\x00\x00\x00\x01" + end + end + describe '#encode_varint' do context 'when the integer is below 0xfd' do let(:integer) { 0x5d } @@ -121,4 +127,10 @@ expect(described_module.from_hex_to_bytes("A02F")).to eq "\xA0/" end end + + describe '#bytes_to_hex' do + it 'takes a byte sequence and returns an hex string' do + expect(described_module.bytes_to_hex("\xA0/")).to eq "a02f" + end + end end From 5a3a96617527fbfaf7133a23ff0eee3dbf9399a8 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Fri, 28 Oct 2022 02:02:29 -0300 Subject: [PATCH 03/17] feat(network): add Envelope Co-authored-by: Maurice Poirrier --- lib/bitcoin/network/envelope.rb | 74 +++++++++++++++++++++++++++ spec/bitcoin/network/envelope_spec.rb | 50 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 lib/bitcoin/network/envelope.rb create mode 100644 spec/bitcoin/network/envelope_spec.rb diff --git a/lib/bitcoin/network/envelope.rb b/lib/bitcoin/network/envelope.rb new file mode 100644 index 0000000..3569be1 --- /dev/null +++ b/lib/bitcoin/network/envelope.rb @@ -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 diff --git a/spec/bitcoin/network/envelope_spec.rb b/spec/bitcoin/network/envelope_spec.rb new file mode 100644 index 0000000..c95dfa5 --- /dev/null +++ b/spec/bitcoin/network/envelope_spec.rb @@ -0,0 +1,50 @@ +# encoding: ascii-8bit + +require 'bitcoin/network/envelope' +require 'encoding_helper' + +RSpec.describe Bitcoin::Network::Envelope do + include EncodingHelper + + let(:raw_envelope) { from_hex_to_bytes("f9beb4d976657261636b000000000000000000005df6e0e2") } + + def parse(_raw_envelope) + described_class.parse(StringIO.new(_raw_envelope)) + end + + describe '.parse' do + it 'properly parses magic hex' do + expect(parse(raw_envelope).magic_hex).to eq Bitcoin::Network::Envelope::NETWORK_MAGIC + end + + it 'properly parses command bytes' do + expect(parse(raw_envelope).command_bytes).to eq "verack" + end + + it 'properly parses payload bytes' do + expect(parse(raw_envelope).payload_bytes).to eq "" + end + end + + describe "#to_s" do + it "returns a string containing the command and the payload" do + expect(parse(raw_envelope).to_s).to(eq("verack: ")) + end + end + + describe "#serialize" do + it "serializes correctly" do + envelope = described_class.new("ping", "\x00\x00\x00\x00\x00\x00\x00\x01", "f9beb4d9") + + expect(envelope.serialize).to( + eq( + "\v\x11\t\aping\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00:\xE5\xC1\x98\x00\x00\x00\x00\x00\x00\x00\x01" + ) + ) + end + + it "serializes same envelope" do + expect(parse(raw_envelope).serialize).to(eq(raw_envelope)) + end + end +end From 1b8e4ffcb803c2f5af296279b7c99ebb57363e35 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Thu, 27 Oct 2022 00:26:40 -0300 Subject: [PATCH 04/17] feat(network): add BaseMessage --- .rubocop.yml | 3 +++ lib/bitcoin/network/messages/base_message.rb | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 lib/bitcoin/network/messages/base_message.rb diff --git a/.rubocop.yml b/.rubocop.yml index 5e180e7..20c17cc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 diff --git a/lib/bitcoin/network/messages/base_message.rb b/lib/bitcoin/network/messages/base_message.rb new file mode 100644 index 0000000..bed5a4c --- /dev/null +++ b/lib/bitcoin/network/messages/base_message.rb @@ -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 From 1d9d0e00c9110856c9984751709403f91f0c9d38 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Fri, 28 Oct 2022 02:03:14 -0300 Subject: [PATCH 05/17] feat(network/messages): add version Co-authored-by: Maurice Poirrier --- lib/bitcoin/network/messages/version.rb | 62 +++++++++++++++++ spec/bitcoin/network/messages/version_spec.rb | 68 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 lib/bitcoin/network/messages/version.rb create mode 100644 spec/bitcoin/network/messages/version_spec.rb diff --git a/lib/bitcoin/network/messages/version.rb b/lib/bitcoin/network/messages/version.rb new file mode 100644 index 0000000..44d4b84 --- /dev/null +++ b/lib/bitcoin/network/messages/version.rb @@ -0,0 +1,62 @@ +# encoding: ascii-8bit +require_relative '../../../bitcoin_data_io' +require_relative './base_message' + +module Bitcoin + module Network + module Messages + class Version < BaseMessage + COMMAND = "version" + + def initialize( + version: 70015, + services: 0, + timestamp: nil, + receiver_services: 0, + receiver_ip: "\x00\x00\x00\x00", + receiver_port: 8333, + sender_services: 0, + sender_ip: "\x00\x00\x00\x00", + sender_port: 8333, + nonce: nil, + user_agent: "/budabitcoinguild:0.1/", + latest_block: 0, + relay: false + ) + @version = version + @services = services + @timestamp = timestamp.nil? ? Time.now.to_i : timestamp + @receiver_services = receiver_services + @receiver_ip = receiver_ip + @receiver_port = receiver_port + @sender_services = sender_services + @sender_ip = sender_ip + @sender_port = sender_port + @nonce = nonce.nil? ? 0 : nonce + @user_agent = user_agent + @latest_block = latest_block + @relay = relay + end + + def serialize + result = int_to_little_endian(@version, 4) + result << int_to_little_endian(@services, 8) + result << int_to_little_endian(@timestamp, 8) + result << int_to_little_endian(@receiver_services, 8) + result << "\x00" * 10 + "\xff\xff" + @receiver_ip + result << int_to_big_endian(@receiver_port, 2) + result << int_to_little_endian(@sender_services, 8) + result << "\x00" * 10 + "\xff\xff" + @sender_ip + result << int_to_big_endian(@sender_port, 2) + result << int_to_little_endian(@nonce, 8) + result << encode_varint(@user_agent.length) + result << @user_agent + result << int_to_little_endian(@latest_block, 4) + result << (@relay ? "\x01" : "\x00") + + result + end + end + end + end +end diff --git a/spec/bitcoin/network/messages/version_spec.rb b/spec/bitcoin/network/messages/version_spec.rb new file mode 100644 index 0000000..e3c6528 --- /dev/null +++ b/spec/bitcoin/network/messages/version_spec.rb @@ -0,0 +1,68 @@ +require 'bitcoin/network/messages/version' +require 'encoding_helper' +require 'timecop' + +RSpec.describe Bitcoin::Network::Messages::Version do + include EncodingHelper + + def serialized_message_hex + bytes_to_hex(described_class.new.serialize) + end + + before do + freezed_time = Time.new(2008, 9, 1, 10, 5, 0, "+00:00") + Timecop.freeze(freezed_time) + end + + describe "#serialize" do + it "serializes version" do + expect(serialized_message_hex.slice(0, 8)).to(eq("7f110100")) + end + + it "serializes services" do + expect(serialized_message_hex.slice(8, 16)).to(eq("0000000000000000")) + end + + it "serializes timestamp" do + expect(serialized_message_hex.slice(24, 16)).to(eq("4cbebb4800000000")) + end + + it "serializes receiver services" do + expect(serialized_message_hex.slice(40, 16)).to(eq("0000000000000000")) + end + + it "serializes receiver ip" do + expect(serialized_message_hex.slice(56, 32)).to(eq("00000000000000000000ffff00000000")) + end + + it "serializes receiver port" do + expect(serialized_message_hex.slice(88, 4)).to(eq("208d")) + end + + it "serializes sender services" do + expect(serialized_message_hex.slice(92, 16)).to(eq("0000000000000000")) + end + + it "serializes sender ip" do + expect(serialized_message_hex.slice(108, 32)).to(eq("00000000000000000000ffff00000000")) + end + + it "serializes sender port" do + expect(serialized_message_hex.slice(140, 4)).to(eq("208d")) + end + + it "serializes nonce" do + expect(serialized_message_hex.slice(144, 16)).to(eq("0000000000000000")) + end + + it "serializes user agent" do + expect(serialized_message_hex.slice(160, 46)).to( + eq("162f62756461626974636f696e6775696c643a302e312f") + ) + end + + it "serializes latest block" do + expect(serialized_message_hex.slice(206, 8)).to(eq("00000000")) + end + end +end From f1d87e72d7ce0a11df33505c411f304c04102254 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Wed, 26 Oct 2022 00:35:10 -0300 Subject: [PATCH 06/17] feat(network/messages): add verack --- lib/bitcoin/network/messages/verack.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lib/bitcoin/network/messages/verack.rb diff --git a/lib/bitcoin/network/messages/verack.rb b/lib/bitcoin/network/messages/verack.rb new file mode 100644 index 0000000..1e79b29 --- /dev/null +++ b/lib/bitcoin/network/messages/verack.rb @@ -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 From 12424b949bcd716af4dfc2d81315253cc8916e87 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Wed, 26 Oct 2022 00:37:25 -0300 Subject: [PATCH 07/17] feat(network): add simpleNode --- lib/bitcoin/network/simple_node.rb | 88 ++++++++++++++++++++++++ spec/bitcoin/network/simple_node_spec.rb | 53 ++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 lib/bitcoin/network/simple_node.rb create mode 100644 spec/bitcoin/network/simple_node_spec.rb diff --git a/lib/bitcoin/network/simple_node.rb b/lib/bitcoin/network/simple_node.rb new file mode 100644 index 0000000..63df0c7 --- /dev/null +++ b/lib/bitcoin/network/simple_node.rb @@ -0,0 +1,88 @@ +require 'socket' +require_relative './envelope' +require_relative './messages/get_headers' +require_relative './messages/headers' +require_relative './messages/ping' +require_relative './messages/pong' +require_relative './messages/verack' +require_relative './messages/version' + +module Bitcoin + module Network + class SimpleNode + TESTNET_PORT = 18333 + MAINNET_PORT = 8333 + + def initialize(host, port=nil, testnet=false, logging=false) + @testnet = testnet + @logging = logging + + port ||= @testnet ? TESTNET_PORT : MAINNET_PORT + + connect(port, host) + end + + attr_accessor :testnet, :logging, :socket + + def send(message) + envelope = Bitcoin::Network::Envelope.new( + message.command, + message.serialize, + @testnet + ) + + puts "sending: #{envelope}" if @logging + + @socket.send(envelope.serialize, 0) + end + + def read + begin + envelope = Bitcoin::Network::Envelope.parse(@socket) + rescue IOError + puts 'no data received' if @logging + return nil + end + + puts "receiving #{envelope}" if @logging + + envelope + end + + def wait_for(*message_classes) + command = nil + command_to_class = message_classes.to_h { |msg_class| [msg_class::COMMAND, msg_class] } + + while !command_to_class.key? command + envelope = read + if envelope.nil? + sleep(1) + next + end + + command = envelope.command_bytes + + send(Bitcoin::Network::Messages::Verack.new) if command == 'version' + send(Bitcoin::Network::Messages::Pong.parse(envelope.stream)) if command == 'ping' + end + + command_to_class[command].parse(envelope.stream) + end + + def handshake + version = Bitcoin::Network::Messages::Version.new + send(version) + wait_for(Bitcoin::Network::Messages::Verack) + end + + private + + def connect(port, host) + @socket = Socket.new Socket::AF_INET, Socket::SOCK_STREAM + + puts 'connecting...' if @logging + @socket.connect(Socket.pack_sockaddr_in(port, host)) + end + end + end +end diff --git a/spec/bitcoin/network/simple_node_spec.rb b/spec/bitcoin/network/simple_node_spec.rb new file mode 100644 index 0000000..5ffd190 --- /dev/null +++ b/spec/bitcoin/network/simple_node_spec.rb @@ -0,0 +1,53 @@ +# encoding: ascii-8bit + +require 'bitcoin/network/simple_node' +require 'encoding_helper' + +RSpec.describe Bitcoin::Network::SimpleNode do + include EncodingHelper + + describe '#wait_for' do + let(:socket) { instance_double(Socket) } + + let(:envelope_verack) { instance_double(Bitcoin::Network::Envelope) } + let(:envelope_another_msg) { instance_double(Bitcoin::Network::Envelope) } + + let(:verack_instance) { instance_double(Bitcoin::Network::Messages::Verack) } + + before do + allow(Socket).to receive(:new).and_return(socket) + allow(Socket).to receive(:pack_sockaddr_in) + allow(socket).to receive(:connect) + + allow(envelope_verack).to receive(:command_bytes).and_return('verack') + allow(envelope_another_msg).to receive(:command_bytes).and_return('another_msg') + + allow(Bitcoin::Network::Envelope).to receive(:parse).and_return( + envelope_another_msg, + envelope_verack + ) + + allow(envelope_verack).to receive(:stream).and_return('stream') + + allow(Bitcoin::Network::Messages::Verack).to receive(:parse).and_return(verack_instance) + end + + it 'loops while the searched message is read' do + message_class = Bitcoin::Network::Messages::Verack + + instance = described_class.new('host', 1111) + instance.wait_for(message_class) + + expect(Bitcoin::Network::Envelope).to have_received(:parse).exactly(2).times + end + + it 'returns the parsed message class' do + message_class = Bitcoin::Network::Messages::Verack + + instance = described_class.new('host', 1111) + result = instance.wait_for(message_class) + + expect(result).to eq(verack_instance) + end + end +end From d9afa1f2b1d01598c792b389c446d7a2e0b98cc5 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Wed, 26 Oct 2022 00:38:15 -0300 Subject: [PATCH 08/17] feat(network/messages): add getHeaders --- lib/bitcoin/block.rb | 4 +++ lib/bitcoin/network/messages/get_headers.rb | 31 +++++++++++++++++++ .../network/messages/get_headers_spec.rb | 30 ++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 lib/bitcoin/network/messages/get_headers.rb create mode 100644 spec/bitcoin/network/messages/get_headers_spec.rb diff --git a/lib/bitcoin/block.rb b/lib/bitcoin/block.rb index 1d0ab0d..5297bf5 100644 --- a/lib/bitcoin/block.rb +++ b/lib/bitcoin/block.rb @@ -7,6 +7,10 @@ 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 diff --git a/lib/bitcoin/network/messages/get_headers.rb b/lib/bitcoin/network/messages/get_headers.rb new file mode 100644 index 0000000..4f1d674 --- /dev/null +++ b/lib/bitcoin/network/messages/get_headers.rb @@ -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 diff --git a/spec/bitcoin/network/messages/get_headers_spec.rb b/spec/bitcoin/network/messages/get_headers_spec.rb new file mode 100644 index 0000000..3f0e22a --- /dev/null +++ b/spec/bitcoin/network/messages/get_headers_spec.rb @@ -0,0 +1,30 @@ +require 'bitcoin/network/messages/get_headers' +require 'bitcoin/block' +require 'encoding_helper' + +RSpec.describe Bitcoin::Network::Messages::GetHeaders do + include EncodingHelper + + def serialized_message_hex + bytes_to_hex(described_class.new(start_block: Bitcoin::Block::TESTNET_GENESIS_BLOCK).serialize) + end + + describe "#serialize" do + it "serializes version" do + expect(serialized_message_hex.slice(0, 8)).to(eq("7f110100")) + end + + it "serializes num_hashes" do + expect(serialized_message_hex.slice(8, 2)).to(eq("01")) + end + + it "serializes start_block" do + expect(serialized_message_hex.slice(10, 160)) + .to(eq(bytes_to_hex(Bitcoin::Block::TESTNET_GENESIS_BLOCK.reverse))) + end + + it "serializes end_block" do + expect(serialized_message_hex.slice(170, 16)).to(eq("0000000000000000")) + end + end +end From 0e725924cb05b757f4e99885bf7850fa9ae968db Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Wed, 26 Oct 2022 00:39:31 -0300 Subject: [PATCH 09/17] feat(network/messages): add headers --- lib/bitcoin/network/messages/headers.rb | 35 +++++++++++++++++++ spec/bitcoin/network/messages/headers_spec.rb | 31 ++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 lib/bitcoin/network/messages/headers.rb create mode 100644 spec/bitcoin/network/messages/headers_spec.rb diff --git a/lib/bitcoin/network/messages/headers.rb b/lib/bitcoin/network/messages/headers.rb new file mode 100644 index 0000000..414c55e --- /dev/null +++ b/lib/bitcoin/network/messages/headers.rb @@ -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 diff --git a/spec/bitcoin/network/messages/headers_spec.rb b/spec/bitcoin/network/messages/headers_spec.rb new file mode 100644 index 0000000..35501d1 --- /dev/null +++ b/spec/bitcoin/network/messages/headers_spec.rb @@ -0,0 +1,31 @@ +require 'bitcoin/network/messages/headers' +require 'bitcoin/block' +require 'encoding_helper' +require 'stringio' + +RSpec.describe Bitcoin::Network::Messages::Headers do + include EncodingHelper + + describe ".parse" do + def stream + StringIO.new(from_hex_to_bytes("0200000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b67d8001ac157e670000000002030eb2540c41025690160a1014c577061596e32e426b712c7ca00000000000000768b89f07044e6130ead292a3f51951adbd2202df447d98789339937fd006bd44880835b67d8001ade09204600")) + end + + before do + allow(Bitcoin::Block).to receive(:parse) + .and_call_original + end + + it "parses all the serialized headers" do + parsed_message = described_class.parse(stream) + + expect(parsed_message.blocks.length).to be(2) + end + + it "creates blocks instances for each header" do + parsed_message = described_class.parse(stream) + + expect(parsed_message.blocks).to all(be_a(Bitcoin::Block)) + end + end +end From 1bf48145a3a6c48f2c3d63909dae0851e679a3a2 Mon Sep 17 00:00:00 2001 From: Federico Taladriz Date: Wed, 26 Oct 2022 00:39:49 -0300 Subject: [PATCH 10/17] feat(network/messages): add ping & pong --- lib/bitcoin/network/messages/ping.rb | 25 +++++++++++++++++++++++++ lib/bitcoin/network/messages/pong.rb | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 lib/bitcoin/network/messages/ping.rb create mode 100644 lib/bitcoin/network/messages/pong.rb diff --git a/lib/bitcoin/network/messages/ping.rb b/lib/bitcoin/network/messages/ping.rb new file mode 100644 index 0000000..3cc4bdc --- /dev/null +++ b/lib/bitcoin/network/messages/ping.rb @@ -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 diff --git a/lib/bitcoin/network/messages/pong.rb b/lib/bitcoin/network/messages/pong.rb new file mode 100644 index 0000000..f701b6b --- /dev/null +++ b/lib/bitcoin/network/messages/pong.rb @@ -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 From 8e95350822b55b78817a54becf007d83b0deec70 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 14:53:29 -0300 Subject: [PATCH 11/17] refactor(spec/bitcoin_data_io): use bytes_to_hex from encoding_helper --- spec/bitcoin_data_io_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/bitcoin_data_io_spec.rb b/spec/bitcoin_data_io_spec.rb index 448d307..e68b187 100644 --- a/spec/bitcoin_data_io_spec.rb +++ b/spec/bitcoin_data_io_spec.rb @@ -1,6 +1,9 @@ +require 'bitcoin_data_io' require 'encoding_helper' RSpec.describe BitcoinDataIO do + include EncodingHelper + def io(_hex_data) described_class.new(StringIO.new([_hex_data].pack("H*"))) end @@ -46,8 +49,4 @@ def io(_hex_data) expect(io('ff0807060504030201ff').read_varint).to eq 0x102030405060708 end end - - def bytes_to_hex(_bytes) - _bytes.unpack1("H*") - end end From b18afd46ca4c9495800668b053aaa26bd8772243 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 14:58:57 -0300 Subject: [PATCH 12/17] feat: add merkle helper --- lib/merkle_helper.rb | 31 +++++++++++ spec/merkle_helper_spec.rb | 104 +++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 lib/merkle_helper.rb create mode 100644 spec/merkle_helper_spec.rb diff --git a/lib/merkle_helper.rb b/lib/merkle_helper.rb new file mode 100644 index 0000000..b979cb8 --- /dev/null +++ b/lib/merkle_helper.rb @@ -0,0 +1,31 @@ +require_relative 'hash_helper' + +module MerkleHelper + def self.merkle_parent(hash1, hash2) + HashHelper.hash256(hash1 + hash2) + end + + def self.merkle_parent_level(hashes) + raise ArgumentError, "List of hashes can't be of size one" if hashes.size == 1 + + if hashes.size.odd? + hashes << hashes.last + end + + parent_level = [] + + (0...hashes.size).step(2) do |i| + parent_level << merkle_parent(hashes[i], hashes[i + 1]) + end + parent_level + end + + def self.merkle_root(hashes) + current_hashes = hashes + + until current_hashes.size == 1 + current_hashes = merkle_parent_level(current_hashes) + end + current_hashes.first + end +end diff --git a/spec/merkle_helper_spec.rb b/spec/merkle_helper_spec.rb new file mode 100644 index 0000000..a7b9cbd --- /dev/null +++ b/spec/merkle_helper_spec.rb @@ -0,0 +1,104 @@ +require 'merkle_helper' +require 'encoding_helper' + +RSpec.describe MerkleHelper do + include EncodingHelper + + describe '#merkle_parent' do + let(:hash1) do + from_hex_to_bytes('c117ea8ec828342f4dfb0ad6bd140e03a50720ece40169ee38bdc15d9eb64cf5') + end + let(:hash2) do + from_hex_to_bytes('c131474164b412e3406696da1ee20ab0fc9bf41c8f05fa8ceea7a08d672d7cc5') + end + + it 'computes the hash of the combined input hashes' do + parent_hash = from_hex_to_bytes( + '8b30c5ba100f6f2e5ad1e2a742e5020491240f8eb514fe97c713c31718ad7ecd' + ) + + expect(described_class.merkle_parent(hash1, hash2)) + .to eq(parent_hash) + end + end + + # rubocop:disable RSpec/ExampleLength + describe '#merkle_parent_level' do + let(:hex_hashes) do + [ + 'c117ea8ec828342f4dfb0ad6bd140e03a50720ece40169ee38bdc15d9eb64cf5', + 'c131474164b412e3406696da1ee20ab0fc9bf41c8f05fa8ceea7a08d672d7cc5', + 'f391da6ecfeed1814efae39e7fcb3838ae0b02c02ae7d0a5848a66947c0727b0', + '3d238a92a94532b946c90e19c49351c763696cff3db400485b813aecb8a13181', + '10092f2633be5f3ce349bf9ddbde36caa3dd10dfa0ec8106bce23acbff637dae', + '7d37b3d54fa6a64869084bfd2e831309118b9e833610e6228adacdbd1b4ba161', + '8118a77e542892fe15ae3fc771a4abfd2f5d5d5997544c3487ac36b5c85170fc', + 'dff6879848c2c9b62fe652720b8df5272093acfaa45a43cdb3696fe2466a3877', + 'b825c0745f46ac58f7d3759e6dc535a1fec7820377f24d4c2c6ad2cc55c0cb59', + '95513952a04bd8992721e9b7e2937f1c04ba31e0469fbe615a78197f68f52b7c', + '2e6d722e5e4dbdf2447ddecc9f7dabb8e299bae921c99ad5b0184cd9eb8e5908' + ] + end + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + + context 'when number of hashes is odd' do + it 'returns the merkle parent level' do + hex_hashes = [ + '8b30c5ba100f6f2e5ad1e2a742e5020491240f8eb514fe97c713c31718ad7ecd', + '7f4e6f9e224e20fda0ae4c44114237f97cd35aca38d83081c9bfd41feb907800', + 'ade48f2bbb57318cc79f3a8678febaa827599c509dce5940602e54c7733332e7', + '68b3e2ab8182dfd646f13fdf01c335cf32476482d963f5cd94e934e6b3401069', + '43e7274e77fbe8e5a42a8fb58f7decdb04d521f319f332d88e6b06f8e6c09e27', + '1796cd3ca4fef00236e07b723d3ed88e1ac433acaaa21da64c4b33c946cf3d10' + ] + bytes_hashes_solution = hex_hashes.map { |h| from_hex_to_bytes(h) } + + expect(described_class.merkle_parent_level(byte_hashes)) + .to eq(bytes_hashes_solution) + end + end + + context 'when number of hashes is even' do + let(:byte_hashes_even) { byte_hashes.first(4) } + + it 'returns the merkle parent level' do + hex_hashes = [ + '8b30c5ba100f6f2e5ad1e2a742e5020491240f8eb514fe97c713c31718ad7ecd', + '7f4e6f9e224e20fda0ae4c44114237f97cd35aca38d83081c9bfd41feb907800' + ] + bytes_hashes_solution = hex_hashes.map { |h| from_hex_to_bytes(h) } + + expect(described_class.merkle_parent_level(byte_hashes_even)) + .to eq(bytes_hashes_solution) + end + end + end + # rubocop:enable RSpec/ExampleLength + + describe '#merkle_root' do + let(:hex_hashes) do + [ + 'c117ea8ec828342f4dfb0ad6bd140e03a50720ece40169ee38bdc15d9eb64cf5', + 'c131474164b412e3406696da1ee20ab0fc9bf41c8f05fa8ceea7a08d672d7cc5', + 'f391da6ecfeed1814efae39e7fcb3838ae0b02c02ae7d0a5848a66947c0727b0', + '3d238a92a94532b946c90e19c49351c763696cff3db400485b813aecb8a13181', + '10092f2633be5f3ce349bf9ddbde36caa3dd10dfa0ec8106bce23acbff637dae', + '7d37b3d54fa6a64869084bfd2e831309118b9e833610e6228adacdbd1b4ba161', + '8118a77e542892fe15ae3fc771a4abfd2f5d5d5997544c3487ac36b5c85170fc', + 'dff6879848c2c9b62fe652720b8df5272093acfaa45a43cdb3696fe2466a3877', + 'b825c0745f46ac58f7d3759e6dc535a1fec7820377f24d4c2c6ad2cc55c0cb59', + '95513952a04bd8992721e9b7e2937f1c04ba31e0469fbe615a78197f68f52b7c', + '2e6d722e5e4dbdf2447ddecc9f7dabb8e299bae921c99ad5b0184cd9eb8e5908', + 'b13a750047bc0bdceb2473e5fe488c2596d7a7124b4e716fdd29b046ef99bbf0' + ] + end + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + + it 'computes the merkle root' do + hex_root = 'acbcab8bcc1af95d8d563b77d24c3d19b18f1486383d75a5085c4e86c86beed6' + bytes = from_hex_to_bytes(hex_root) + + expect(described_class.merkle_root(byte_hashes)).to eq(bytes) + end + end +end From ea9b795fd90c1bedcd164bd9ef2762466acc3763 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 15:08:22 -0300 Subject: [PATCH 13/17] feat: add merkle tree --- lib/merkle_tree.rb | 117 +++++++++++++++++++++++++++++++++++ spec/merkle_tree_spec.rb | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 lib/merkle_tree.rb create mode 100644 spec/merkle_tree_spec.rb diff --git a/lib/merkle_tree.rb b/lib/merkle_tree.rb new file mode 100644 index 0000000..8c49366 --- /dev/null +++ b/lib/merkle_tree.rb @@ -0,0 +1,117 @@ +require_relative './merkle_helper' +require_relative 'encoding_helper' + +class MerkleTree + include EncodingHelper + + def initialize(total) + @total = total + @max_depth = Math.log2(@total).ceil + @current_index = 0 + @current_depth = 0 + + build_nodes + end + + attr_reader :nodes, :current_index, :current_depth, :total, :max_depth + + def to_s + result = [] + @nodes.each do |level| + items = [] + level.each do |hash| + items << (hash.nil? ? "None" : bytes_to_hex(hash)[...8].to_s) + end + result << items.join(', ') + end + result.join("\n") + end + + def populate_tree(flag_bits, hashes) + until root + next handle_leaf(flag_bits, hashes) if leaf? + + left_hash = get_left_node + next handle_left(flag_bits, hashes) if left_hash.nil? + next handle_right(left_hash, get_right_node) if right_exists? + + set_current_node(MerkleHelper.merkle_parent(left_hash, left_hash)) + up + end + + raise "Not all hashes consumed (#{hashes.size})" if hashes.size.positive? + raise 'Not all flag bits consumed' unless flag_bits.all?(0) + end + + def root + @nodes[0][0] + end + + private + + def up + @current_depth -= 1 + @current_index /= 2 + end + + def left + @current_depth += 1 + @current_index *= 2 + end + + def right + @current_depth += 1 + @current_index = @current_index * 2 + 1 + end + + def set_current_node(value) + @nodes[@current_depth][@current_index] = value + end + + def get_current_node + @nodes[@current_depth][@current_index] + end + + def get_left_node + @nodes[@current_depth + 1][@current_index * 2] + end + + def get_right_node + @nodes[@current_depth + 1][@current_index * 2 + 1] + end + + def leaf? + @current_depth == @max_depth + end + + def right_exists? + @nodes[@current_depth + 1].size > @current_index * 2 + 1 + end + + def handle_left(flag_bits, hashes) + return left unless flag_bits.shift.zero? + + set_current_node(hashes.shift) + up + end + + def handle_right(left_hash, right_hash) + return right if right_hash.nil? + + set_current_node(MerkleHelper.merkle_parent(left_hash, right_hash)) + up + end + + def handle_leaf(flag_bits, hashes) + flag_bits.shift + set_current_node(hashes.shift) + up + end + + def build_nodes + @nodes = (0..@max_depth).map do |depth| + num_items = (@total.to_f / 2**(max_depth - depth)).ceil + [nil] * num_items + end + end +end diff --git a/spec/merkle_tree_spec.rb b/spec/merkle_tree_spec.rb new file mode 100644 index 0000000..9a090d3 --- /dev/null +++ b/spec/merkle_tree_spec.rb @@ -0,0 +1,128 @@ +require 'merkle_tree' +require 'encoding_helper' + +RSpec.describe MerkleTree do + include EncodingHelper + + describe '#initialize' do + context 'when number of leaves is even' do + let(:tree) { described_class.new(16) } + + it 'creates tree with correct number of nodes' do + expect(tree.nodes.flatten.size).to be(31) + end + + it 'creates tree with correct max depth' do + expect(tree.max_depth).to be(4) + end + end + + context 'when number of leaves is odd' do + let(:tree) { described_class.new(27) } + + it 'creates tree with correct number of nodes' do + expect(tree.nodes.flatten.size).to be(55) + end + end + end + + describe '#to_s' do + context 'when tree is empty' do + it 'properly represents tree' do + tree_string = "None\n"\ + "None, None\n"\ + "None, None, None, None\n"\ + "None, None, None, None, None, None, None, None" + + expect(described_class.new(8).to_s).to eq(tree_string) + end + end + + context 'when tree is full' do + let(:hex_hashes) do + [ + "42f6f52f17620653dcc909e58bb352e0bd4bd1381e2955d19c00959a22122b2e", + "94c3af34b9667bf787e1c6a0a009201589755d01d02fe2877cc69b929d2418d4", + "959428d7c48113cb9149d0566bde3d46e98cf028053c522b8fa8f735241aa953", + "a9f27b99d5d108dede755710d4a1ffa2c74af70b4ca71726fa57d68454e609a2", + "62af110031e29de1efcad103b3ad4bec7bdcf6cb9c9f4afdd586981795516577" + ] + end + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + let(:flag_bits) { Array.new(11, 1) } + let(:tree) { described_class.new(hex_hashes.size) } + + it 'properly represents tree' do + tree.populate_tree(flag_bits, byte_hashes) + tree_string = "a8e8bd02\n"\ + "e2e5af20, 4e1f780d\n"\ + "d7fb8a90, 4e7a9ff0, 610b17d5\n"\ + "42f6f52f, 94c3af34, 959428d7, a9f27b99, 62af1100" + + expect(tree.to_s).to eq(tree_string) + end + end + end + + describe '#populate_tree' do + context 'when all hashes are given' do + let(:hex_hashes) do + [ + "9745f7173ef14ee4155722d1cbf13304339fd00d900b759c6f9d58579b5765fb", + "5573c8ede34936c29cdfdfe743f7f5fdfbd4f54ba0705259e62f39917065cb9b", + "82a02ecbb6623b4274dfcab82b336dc017a27136e08521091e443e62582e8f05", + "507ccae5ed9b340363a0e6d765af148be9cb1c8766ccc922f83e4ae681658308", + "a7a4aec28e7162e1e9ef33dfa30f0bc0526e6cf4b11a576f6c5de58593898330", + "bb6267664bd833fd9fc82582853ab144fece26b7a8a5bf328f8a059445b59add", + "ea6d7ac1ee77fbacee58fc717b990c4fcccf1b19af43103c090f601677fd8836", + "457743861de496c429912558a106b810b0507975a49773228aa788df40730d41", + "7688029288efc9e9a0011c960a6ed9e5466581abf3e3a6c26ee317461add619a", + "b1ae7f15836cb2286cdd4e2c37bf9bb7da0a2846d06867a429f654b2e7f383c9", + "9b74f89fa3f93e71ff2c241f32945d877281a6a50a6bf94adac002980aafe5ab", + "b3a92b5b255019bdaf754875633c2de9fec2ab03e6b8ce669d07cb5b18804638", + "b5c0b915312b9bdaedd2b86aa2d0f8feffc73a2d37668fd9010179261e25e263", + "c9d52c5cb1e557b92c84c52e7c4bfbce859408bedffc8a5560fd6e35e10b8800", + "c555bc5fc3bc096df0a0c9532f07640bfb76bfe4fc1ace214b8b228a1297a4c2", + "f9dbfafc3af3400954975da24eb325e326960a25b87fffe23eef3e7ed2fb610e" + ] + end + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + let(:flag_bits) { Array.new(31, 1) } + let(:tree) { described_class.new(hex_hashes.size) } + + it 'properly populates the tree' do + tree.populate_tree(flag_bits, byte_hashes) + root = '597c4bafe3832b17cbbabe56f878f4fc2ad0f6a402cee7fa851a9cb205f87ed1' + + expect(bytes_to_hex(tree.root)).to eq(root) + end + end + + context 'when some hashes are given' do + let(:hex_hashes) do + [ + 'ba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a', + '7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d', + '34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2', + '158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cba', + 'ee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763ce', + 'f8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097', + 'c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d', + '6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543', + 'd1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274c', + 'dfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb62261' + ] + end + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + let(:flag_bits) { [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0] } + let(:tree) { described_class.new(3519) } + + it 'properly populates the tree' do + tree.populate_tree(flag_bits, byte_hashes) + root_solution = 'ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4' + + expect(bytes_to_hex(tree.root)).to eq(root_solution) + end + end + end +end From 45b4027fa19e9328aa1ad5224da52d1de7212024 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 15:10:24 -0300 Subject: [PATCH 14/17] feat(block): add merkle_valid? method --- lib/bitcoin/block.rb | 13 +++++++++-- spec/bitcoin/block_spec.rb | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/bitcoin/block.rb b/lib/bitcoin/block.rb index 5297bf5..a3ac514 100644 --- a/lib/bitcoin/block.rb +++ b/lib/bitcoin/block.rb @@ -1,6 +1,7 @@ require_relative '../bitcoin_data_io' require_relative '../encoding_helper' require_relative '../hash_helper' +require_relative '../merkle_helper' module Bitcoin class Block @@ -14,16 +15,17 @@ class Block 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) @@ -107,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 diff --git a/spec/bitcoin/block_spec.rb b/spec/bitcoin/block_spec.rb index 3bd9458..601036b 100644 --- a/spec/bitcoin/block_spec.rb +++ b/spec/bitcoin/block_spec.rb @@ -138,4 +138,48 @@ def parse(*args) it { expect(block_header.pow_valid?).to be false } end end + + describe '#merkle_root_valid?' do + let(:raw_block_header) do + from_hex_to_bytes( + "00000020fcb19f7895db08cadc9573e7915e3919fb76d59868a51d995201000000000000acbcab8"\ + "bcc1af95d8d563b77d24c3d19b18f1486383d75a5085c4e86c86beed691cfa85916ca061a00000000" + ) + end + + let(:block_header) { described_class.parse(StringIO.new(*raw_block_header)) } + let(:hex_hashes) do + [ + 'f54cb69e5dc1bd38ee6901e4ec2007a5030e14bdd60afb4d2f3428c88eea17c1', + 'c57c2d678da0a7ee8cfa058f1cf49bfcb00ae21eda966640e312b464414731c1', + 'b027077c94668a84a5d0e72ac0020bae3838cb7f9ee3fa4e81d1eecf6eda91f3', + '8131a1b8ec3a815b4800b43dff6c6963c75193c4190ec946b93245a9928a233d', + 'ae7d63ffcb3ae2bc0681eca0df10dda3ca36dedb9dbf49e33c5fbe33262f0910', + '61a14b1bbdcdda8a22e61036839e8b110913832efd4b086948a6a64fd5b3377d', + 'fc7051c8b536ac87344c5497595d5d2ffdaba471c73fae15fe9228547ea71881', + '77386a46e26f69b3cd435aa4faac932027f58d0b7252e62fb6c9c2489887f6df', + '59cbc055ccd26a2c4c4df2770382c7fea135c56d9e75d3f758ac465f74c025b8', + '7c2bf5687f19785a61be9f46e031ba041c7f93e2b7e9212799d84ba052395195', + '08598eebd94c18b0d59ac921e9ba99e2b8ab7d9fccde7d44f2bd4d5e2e726d2e', + 'f0bb99ef46b029dd6f714e4b12a7d796258c48fee57324ebdc0bbc4700753ab1' + ] + end + + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h) } } + + context 'with valid tx _hashes' do + before { block_header.tx_hashes = byte_hashes } + + it { expect(block_header.merkle_root_valid?).to be true } + end + + context 'with invalid tx _hashes' do + before do + byte_hashes[-1] = from_hex_to_bytes('00000000') + block_header.tx_hashes = byte_hashes + end + + it { expect(block_header.pow_valid?).to be false } + end + end end From 6722d0e58bc09c138d69f53fea38602679712764 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 15:12:01 -0300 Subject: [PATCH 15/17] feat(encoding_helper): add bytes_to_bit_field method --- lib/encoding_helper.rb | 11 +++++++++++ spec/encoding_helper_spec.rb | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/encoding_helper.rb b/lib/encoding_helper.rb index 1ca63ee..d058474 100644 --- a/lib/encoding_helper.rb +++ b/lib/encoding_helper.rb @@ -28,6 +28,17 @@ def to_bytes(integer, bytes, endianness) byte_array.pack('c*') end + def bytes_to_bit_field(bytes) + bit_field = [] + bytes.each_byte do |byte| + 8.times do + bit_field << (byte & 1) + byte >>= 1 + end + end + bit_field + end + def encode_base58(bytes) zero_prefix_length = 0 bytes.each_char { |char| char == "\x00" ? zero_prefix_length += 1 : break } diff --git a/spec/encoding_helper_spec.rb b/spec/encoding_helper_spec.rb index 809059b..3c70775 100644 --- a/spec/encoding_helper_spec.rb +++ b/spec/encoding_helper_spec.rb @@ -1,4 +1,5 @@ # encoding: ascii-8bit + require 'encoding_helper' RSpec.describe EncodingHelper do @@ -133,4 +134,13 @@ expect(described_module.bytes_to_hex("\xA0/")).to eq "a02f" end end + + describe '#bytes_to_bit_field' do + let(:bytes) { described_module.from_hex_to_bytes("b55635") } + + it 'takes a string of bytes and returns a bit field array' do + expect(described_module.bytes_to_bit_field(bytes)) + .to eq [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0] + end + end end From f9fbc66270d48bd7d17850bfbe9463bde362cfcc Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 15:14:40 -0300 Subject: [PATCH 16/17] feat: add merkle block --- lib/bitcoin/merkle_block.rb | 55 +++++++++++++++ spec/bitcoin/merkle_block_spec.rb | 108 ++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 lib/bitcoin/merkle_block.rb create mode 100644 spec/bitcoin/merkle_block_spec.rb diff --git a/lib/bitcoin/merkle_block.rb b/lib/bitcoin/merkle_block.rb new file mode 100644 index 0000000..a14c1da --- /dev/null +++ b/lib/bitcoin/merkle_block.rb @@ -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 diff --git a/spec/bitcoin/merkle_block_spec.rb b/spec/bitcoin/merkle_block_spec.rb new file mode 100644 index 0000000..5e5adc0 --- /dev/null +++ b/spec/bitcoin/merkle_block_spec.rb @@ -0,0 +1,108 @@ +require 'bitcoin/merkle_block' +require 'encoding_helper' + +RSpec.describe Bitcoin::MerkleBlock do + include EncodingHelper + + let(:raw_merkle_block) do + from_hex_to_bytes( + "00000020df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000"\ + "ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4dc7c835b6"\ + "7d8001ac157e670bf0d00000aba412a0d1480e370173072c9562becffe87aa661c1e4a6db"\ + "c305d38ec5dc088a7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655"\ + "ee1e27d34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c215"\ + "8785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cbaee7261831e1"\ + "550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763cef8e625f977af7c8619c3"\ + "2a369b832bc2d051ecd9c73c51e76370ceabd4f25097c256597fa898d404ed53425de608a"\ + "c6bfe426f6e2bb457f1c554866eb69dcb8d6bf6f880e9a59b3cd053e6c7060eeacaacf4da"\ + "c6697dac20e4bd3f38a2ea2543d1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848"\ + "384eebe9af917274cdfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a41"\ + "1cb6226103b55635" + ) + end + + let(:hex_hashes) do + [ + 'ba412a0d1480e370173072c9562becffe87aa661c1e4a6dbc305d38ec5dc088a', + '7cf92e6458aca7b32edae818f9c2c98c37e06bf72ae0ce80649a38655ee1e27d', + '34d9421d940b16732f24b94023e9d572a7f9ab8023434a4feb532d2adfc8c2c2', + '158785d1bd04eb99df2e86c54bc13e139862897217400def5d72c280222c4cba', + 'ee7261831e1550dbb8fa82853e9fe506fc5fda3f7b919d8fe74b6282f92763ce', + 'f8e625f977af7c8619c32a369b832bc2d051ecd9c73c51e76370ceabd4f25097', + 'c256597fa898d404ed53425de608ac6bfe426f6e2bb457f1c554866eb69dcb8d', + '6bf6f880e9a59b3cd053e6c7060eeacaacf4dac6697dac20e4bd3f38a2ea2543', + 'd1ab7953e3430790a9f81e1c67f5b58c825acf46bd02848384eebe9af917274c', + 'dfbb1a28a5d58a23a17977def0de10d644258d9c54f886d47d293a411cb62261' + ] + end + + let(:byte_hashes) { hex_hashes.map { |h| from_hex_to_bytes(h).reverse } } + + let(:merkle_block_solution) do + described_class.new( + 0x20000000, + from_hex_to_bytes('df3b053dc46f162a9b00c7f0d5124e2676d47bbe7c5d0793a500000000000000').reverse, + from_hex_to_bytes('ef445fef2ed495c275892206ca533e7411907971013ab83e3b47bd0d692d14d4').reverse, + little_endian_to_int(from_hex_to_bytes('dc7c835b')), + from_hex_to_bytes('67d8001a'), + from_hex_to_bytes('c157e670'), + little_endian_to_int(from_hex_to_bytes('bf0d0000')), + byte_hashes, + from_hex_to_bytes('b55635').reverse + ) + end + + describe '.parse' do + def parse(*args) + described_class.parse(StringIO.new(*args)) + end + + it 'properly parses the version' do + expect(parse(raw_merkle_block).version).to eq merkle_block_solution.version + end + + it 'properly parses the previous block' do + expect(parse(raw_merkle_block).prev_block).to eq merkle_block_solution.prev_block + end + + it 'properly parses the merkle root' do + expect(parse(raw_merkle_block).merkle_root).to eq merkle_block_solution.merkle_root + end + + it 'properly parses the timestamp' do + expect(parse(raw_merkle_block).timestamp).to eq merkle_block_solution.timestamp + end + + it 'properly parses the bits' do + expect(parse(raw_merkle_block).bits).to eq merkle_block_solution.bits + end + + it 'properly parses the nonce' do + expect(parse(raw_merkle_block).nonce).to eq merkle_block_solution.nonce + end + + it 'properly parses the total_tx' do + expect(parse(raw_merkle_block).total_tx).to eq merkle_block_solution.total_tx + end + + it 'properly parses the tx_hashes' do + expect(parse(raw_merkle_block).tx_hashes).to eq merkle_block_solution.tx_hashes + end + + it 'properly parses the flags' do + expect(parse(raw_merkle_block).flags).to eq merkle_block_solution.flags + end + end + + describe '#valid?' do + context 'with valid merkle root' do + it { expect(merkle_block_solution.valid?).to be true } + end + + context 'with invalid merkle root' do + before { merkle_block_solution.merkle_root = from_hex_to_bytes('00000000') } + + it { expect(merkle_block_solution.valid?).to be false } + end + end +end From a985cdb6cddb74d4f92aeefc16a16beea20bb1b7 Mon Sep 17 00:00:00 2001 From: Eduardo Cifuentes Date: Wed, 26 Oct 2022 15:16:43 -0300 Subject: [PATCH 17/17] chore(gemfile): add pry-byebug --- Gemfile | 5 +++-- Gemfile.lock | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index c706e16..9102847 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ source "https://rubygems.org" # gem "rails" -gem "pry", groups: [:development] -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] diff --git a/Gemfile.lock b/Gemfile.lock index 54f107a..69e4677 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -29,8 +33,9 @@ PLATFORMS DEPENDENCIES pry + pry-byebug rspec (~> 3.11) timecop (~> 0.9.5) BUNDLED WITH - 2.3.16 + 2.3.20