Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to bulk-verify block states hashes #20

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
97 changes: 77 additions & 20 deletions app/models/eth_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BlockNotReadyToImportError < StandardError; end
inverse_of: :eth_block
end

before_validation :generate_attestation_hash, if: -> { imported_at.present? }
before_validation :set_attestation_hash, if: -> { imported_at.present? }

def self.find_by_page_key(...)
find_by_block_number(...)
Expand Down Expand Up @@ -253,49 +253,106 @@ def self.next_blocks_to_import(n)
(max_db_block + 1..max_db_block + n).to_a
end

def generate_attestation_hash
def calculate_attestation_hash
hash = Digest::SHA256.new

self.parent_state_hash = EthBlock.where(block_number: block_number - 1).

parent_block_number = if is_genesis_block?
self.class.genesis_blocks.select { |b| b < block_number }.max
else
block_number - 1
end

parent_state_hash = EthBlock.where(block_number: parent_block_number).
limit(1).pluck(:state_hash).first

hash << parent_state_hash.to_s

hash << hashable_attributes.map do |attr|
send(attr)
end.to_json

associations_to_hash.each do |association|
hashable_attributes = quoted_hashable_attributes(association.klass)
records = association_scope(association).pluck(*hashable_attributes)

hash << records.to_json
end

self.state_hash = "0x" + hash.hexdigest

{
state_hash: "0x" + hash.hexdigest,
parent_state_hash: parent_state_hash
}
end

def self.recalculate_all_state_hashes
total_blocks = EthBlock.count
processed_blocks = 0

associations = associations_to_hash.map(&:name)
EthBlock.includes(*associations).find_each do |block|
res = block.calculate_attestation_hash
block.update_columns(state_hash: res[:state_hash], parent_state_hash: res[:parent_state_hash])

processed_blocks += 1
print "\rProgress: #{processed_blocks}/#{total_blocks} blocks recalculated."
end
puts
end

def self.verify_all_state_hashes
total_blocks = EthBlock.count
processed_blocks = 0
previous_block = nil

associations = associations_to_hash.map(&:name)
EthBlock.includes(*associations).find_each do |block|
res = block.calculate_attestation_hash

if res[:state_hash] != block.state_hash
raise "Mismatched state hash for block #{block.block_number}. Expected: #{block.state_hash}, got: #{res[:state_hash]}"
elsif block.block_number != genesis_blocks.first && res[:parent_state_hash] != previous_block&.state_hash
actual = previous_block&.state_hash
expected = parent_state_hash
raise "Mismatched parent state hash for block #{block.block_number}. Actual: #{actual}, expected: #{expected}"
end

processed_blocks += 1
print "\rProgress: #{processed_blocks}/#{total_blocks} blocks verified."

previous_block = block
end
puts
puts "Final state hash: #{previous_block.state_hash}" if previous_block
end

def set_attestation_hash
res = calculate_attestation_hash

self.state_hash = res[:state_hash]
self.parent_state_hash = res[:parent_state_hash]
end

delegate :quoted_hashable_attributes, :associations_to_hash, to: :class

def hashable_attributes
self.class.hashable_attributes(self.class)
end

def check_attestation_hash
current_hash = state_hash

current_hash == generate_attestation_hash &&
parent_state_hash == EthBlock.find_by(block_number: block_number - 1)&.generate_attestation_hash
ensure
self.state_hash = current_hash
end

def association_scope(association)
association.klass.oldest_first.where(block_number: block_number)
end

def self.associations_to_hash
reflect_on_all_associations(:has_many).sort_by(&:name)
desired_associations = [
:eth_transactions,
:ethscription_ownership_versions,
:ethscription_transfers,
:ethscriptions
].sort

reflect_on_all_associations(:has_many).select do |assoc|
desired_associations.include?(assoc.name)
end.sort_by(&:name)
end

def self.all_hashable_attrs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class UpdateCheckBlockOrderOnUpdateTrigger < ActiveRecord::Migration[7.1]
def up
execute <<-SQL
DROP TRIGGER IF EXISTS trigger_check_block_order_on_update ON eth_blocks;

CREATE OR REPLACE FUNCTION check_block_order_on_update()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
END IF;

IF (SELECT MAX(block_number) FROM eth_blocks) IS NOT NULL THEN
IF NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = (SELECT MAX(block_number) FROM eth_blocks WHERE block_number < NEW.block_number) AND imported_at IS NOT NULL) THEN
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
END IF;
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_check_block_order_on_update
BEFORE UPDATE OF imported_at ON eth_blocks
FOR EACH ROW WHEN (NEW.imported_at IS NOT NULL)
EXECUTE FUNCTION check_block_order_on_update();
SQL
end

def down
execute <<-SQL
DROP TRIGGER IF EXISTS trigger_check_block_order_on_update ON eth_blocks;

CREATE OR REPLACE FUNCTION check_block_order_on_update()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
END IF;

IF NEW.is_genesis_block = false AND
NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = NEW.block_number - 1 AND imported_at IS NOT NULL) THEN
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_check_block_order_on_update
BEFORE UPDATE OF imported_at ON eth_blocks
FOR EACH ROW WHEN (NEW.imported_at IS NOT NULL)
EXECUTE FUNCTION check_block_order_on_update();
SQL
end
end
56 changes: 15 additions & 41 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,6 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: heroku_ext; Type: SCHEMA; Schema: -; Owner: -
--

CREATE SCHEMA heroku_ext;


--
-- Name: SCHEMA public; Type: COMMENT; Schema: -; Owner: -
--

COMMENT ON SCHEMA public IS '';


--
-- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA heroku_ext;


--
-- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner: -
--

COMMENT ON EXTENSION pg_stat_statements IS 'track planning and execution statistics of all SQL statements executed';


--
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
--
Expand Down Expand Up @@ -105,19 +77,20 @@ CREATE FUNCTION public.check_block_order() RETURNS trigger
CREATE FUNCTION public.check_block_order_on_update() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
END IF;

IF NEW.is_genesis_block = false AND
NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = NEW.block_number - 1 AND imported_at IS NOT NULL) THEN
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
END IF;

RETURN NEW;
END;
$$;
BEGIN
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
END IF;

IF (SELECT MAX(block_number) FROM eth_blocks) IS NOT NULL THEN
IF NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = (SELECT MAX(block_number) FROM eth_blocks WHERE block_number < NEW.block_number) AND imported_at IS NOT NULL) THEN
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
END IF;
END IF;

RETURN NEW;
END;
$$;


--
Expand Down Expand Up @@ -1462,6 +1435,7 @@ ALTER TABLE ONLY public.token_items
SET search_path TO "$user", public;

INSERT INTO "schema_migrations" (version) VALUES
('20240130220537'),
('20240126184612'),
('20240126162132'),
('20240115192312'),
Expand Down