diff --git a/app/models/eth_block.rb b/app/models/eth_block.rb index bdc534c..cf8331a 100644 --- a/app/models/eth_block.rb +++ b/app/models/eth_block.rb @@ -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(...) @@ -253,26 +253,83 @@ 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 @@ -280,22 +337,22 @@ def generate_attestation_hash 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 diff --git a/db/migrate/20240130220537_update_check_block_order_on_update_trigger.rb b/db/migrate/20240130220537_update_check_block_order_on_update_trigger.rb new file mode 100644 index 0000000..7aa4ac1 --- /dev/null +++ b/db/migrate/20240130220537_update_check_block_order_on_update_trigger.rb @@ -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 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a8c2cdc..880805d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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: - -- @@ -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; + $$; -- @@ -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'),