Skip to content
Merged
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
46 changes: 46 additions & 0 deletions app/models/eth_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ class BlockNotReadyToImportError < StandardError; end
inverse_of: :eth_block
has_many :ethscription_transfers, foreign_key: :block_number, primary_key: :block_number,
inverse_of: :eth_block
has_many :ethscription_ownership_versions, foreign_key: :block_number, primary_key: :block_number,
inverse_of: :eth_block

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

def self.genesis_blocks
blocks = if ENV.fetch('ETHEREUM_NETWORK') == "eth-mainnet"
[1608625, 3369985, 3981254, 5873780, 8205613, 9046950,
Expand Down Expand Up @@ -236,4 +240,46 @@ def as_json(options = {})
)
).with_indifferent_access
end

def generate_attestation_hash
hash = Digest::SHA256.new

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

hash << (parent_state_hash || "NULL")
hash << hashable_attributes(self.class).map { |attr| send(attr) }.
map { |record| record.nil? ? 'NULL' : record }.join

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

records.map! { |record| record.nil? ? 'NULL' : record }
hash << records.join
end

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

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

def associations_to_hash
self.class.reflect_on_all_associations(:has_many)
end

def hashable_attributes(klass)
klass.columns_hash.reject do |k, v|
v.type == :datetime || ['id'].include?(k)
end.keys.sort
end

def quoted_hashable_attributes(klass)
hashable_attributes(klass).map do |attr|
Arel.sql("encode(digest(#{klass.connection.quote_column_name(attr)}::text, 'sha256'), 'hex')")
end
end
end
75 changes: 30 additions & 45 deletions app/models/eth_transaction.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@

class EthTransaction < ApplicationRecord
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number, optional: true,
inverse_of: :eth_transaction
inverse_of: :eth_block
has_one :ethscription, foreign_key: :transaction_hash, primary_key: :transaction_hash,
inverse_of: :eth_transaction, dependent: :destroy

inverse_of: :eth_transaction
has_many :ethscription_transfers, foreign_key: :transaction_hash,
primary_key: :transaction_hash, dependent: :destroy, inverse_of: :eth_transaction
primary_key: :transaction_hash, inverse_of: :eth_transaction
has_many :ethscription_ownership_versions, foreign_key: :transaction_hash,
primary_key: :transaction_hash, inverse_of: :eth_transaction

attr_accessor :transfer_index

scope :newest_first, -> { order(block_number: :desc, transaction_index: :desc) }
scope :oldest_first, -> { order(block_number: :asc, transaction_index: :asc) }

def self.event_signature(event_name)
"0x" + Digest::Keccak256.hexdigest(event_name)
end

CreateEthscriptionEventSig = event_signature("ethscriptions_protocol_CreateEthscription(address,string)")
Esip2EventSig = event_signature("ethscriptions_protocol_TransferEthscriptionForPreviousOwner(address,address,bytes32)")
Esip1EventSig = event_signature("ethscriptions_protocol_TransferEthscription(address,bytes32)")

def possibly_relevant?
status != 0 &&
(possibly_creates_ethscription? || possibly_transfers_ethscription?)
Expand Down Expand Up @@ -104,7 +116,7 @@ def ethscription_creation_events
return [] unless EthTransaction.esip3_enabled?(block_number)

ordered_events.select do |log|
EthTransaction.contracts_create_ethscription_event_sig == log['topics'].first
CreateEthscriptionEventSig == log['topics'].first
end
end

Expand Down Expand Up @@ -143,16 +155,17 @@ def create_ethscription_transfers_from_events!
topics = log['topics']
event_type = topics.first

if event_type == EthTransaction.esip1_transfer_event_signature
if event_type == Esip1EventSig
begin
event_to = Eth::Abi.decode(['address'], topics.third).first
event_to = Eth::Abi.decode(['address'], topics.second).first
tx_hash = Eth::Util.bin_to_prefixed_hex(
Eth::Abi.decode(['bytes32'], topics.third).first
)
rescue Eth::Abi::DecodingError
next
end

next unless valid_bytes32?(topics.third)

target_ethscription = Ethscription.find_by(transaction_hash: topics.third)
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)

if target_ethscription.present?
ethscription_transfers.create!(
Expand All @@ -165,17 +178,18 @@ def create_ethscription_transfers_from_events!
}.merge(transfer_attrs)
)
end
elsif event_type == EthTransaction.esip2_transfer_event_signature
elsif event_type == Esip2EventSig
begin
event_previous_owner = Eth::Abi.decode(['address'], topics.second).first
event_to = Eth::Abi.decode(['address'], topics.third).first
tx_hash = Eth::Util.bin_to_prefixed_hex(
Eth::Abi.decode(['bytes32'], topics.fourth).first
)
rescue Eth::Abi::DecodingError
next
end

next unless valid_bytes32?(topics.fourth)

target_ethscription = Ethscription.find_by(transaction_hash: topics.fourth)
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)

if target_ethscription.present?
ethscription_transfers.create!(
Expand Down Expand Up @@ -277,40 +291,11 @@ def self.esip1_enabled?(block_number)
block_number >= 17672762
end

def valid_bytes32?(value)
/\A0x[0-9a-f]{64}\z/i.match?(value.to_s)
end

def self.contract_transfer_event_signatures(block_number)
[].tap do |res|
res << esip1_transfer_event_signature if esip1_enabled?(block_number)
res << esip2_transfer_event_signature if esip2_enabled?(block_number)
end
end

class << self
extend Memoist

def contracts_create_ethscription_event_sig
"0x" + Digest::Keccak256.hexdigest(
"ethscriptions_protocol_CreateEthscription(address,string)"
)
end
memoize :contracts_create_ethscription_event_sig

def esip2_transfer_event_signature
"0x" + Digest::Keccak256.hexdigest(
"ethscriptions_protocol_TransferEthscriptionForPreviousOwner(address,address,bytes32)"
)
end
memoize :esip2_transfer_event_signature

def esip1_transfer_event_signature
"0x" + Digest::Keccak256.hexdigest(
"ethscriptions_protocol_TransferEthscription(address,bytes32)"
)
res << Esip1EventSig if esip1_enabled?(block_number)
res << Esip2EventSig if esip2_enabled?(block_number)
end
memoize :esip1_transfer_event_signature
end

def self.prune_transactions
Expand Down
10 changes: 8 additions & 2 deletions app/models/ethscription_ownership_version.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class EthscriptionOwnershipVersion < ApplicationRecord
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number, optional: true,
inverse_of: :ethscription_ownership_versions
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number,
optional: true, inverse_of: :ethscription_ownership_versions
belongs_to :eth_transaction,
foreign_key: :transaction_hash,
primary_key: :transaction_hash, optional: true,
Expand All @@ -15,4 +15,10 @@ class EthscriptionOwnershipVersion < ApplicationRecord
transaction_index: :desc,
transfer_index: :desc
)}

scope :oldest_first, -> { order(
block_number: :asc,
transaction_index: :asc,
transfer_index: :asc
)}
end
12 changes: 12 additions & 0 deletions app/models/ethscription_transfer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ class EthscriptionTransfer < ApplicationRecord

after_create :create_ownership_version!, :notify_eth_transaction

scope :newest_first, -> { order(
block_number: :desc,
transaction_index: :desc,
transfer_index: :desc
)}

scope :oldest_first, -> { order(
block_number: :asc,
transaction_index: :asc,
transfer_index: :asc
)}

def notify_eth_transaction
if eth_transaction.transfer_index.nil?
raise "Need eth_transaction.transfer_index"
Expand Down
32 changes: 32 additions & 0 deletions db/migrate/20231216161930_create_eth_blocks.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
class CreateEthBlocks < ActiveRecord::Migration[7.1]
def change
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')

create_table :eth_blocks, force: :cascade do |t|
t.bigint :block_number, null: false
t.bigint :timestamp, null: false
t.string :blockhash, null: false
t.string :parent_blockhash, null: false
t.datetime :imported_at
t.string :state_hash
t.string :parent_state_hash

t.boolean :is_genesis_block, null: false

t.index :block_number, unique: true
t.index :blockhash, unique: true
t.index :imported_at
t.index [:imported_at, :block_number]
t.index :parent_blockhash, unique: true
t.index :state_hash, unique: true
t.index :parent_state_hash, unique: true
t.index :timestamp

t.check_constraint "blockhash ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "parent_blockhash ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "state_hash ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "parent_state_hash ~ '^0x[a-f0-9]{64}$'"

t.timestamps
end
Expand Down Expand Up @@ -46,6 +55,29 @@ def change
FOR EACH ROW EXECUTE FUNCTION check_block_order();
SQL

execute <<~SQL
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

execute <<-SQL
CREATE OR REPLACE FUNCTION delete_later_blocks()
RETURNS TRIGGER AS $$
Expand Down
6 changes: 4 additions & 2 deletions db/migrate/20231216163233_create_eth_transactions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ def change

t.index [:block_number, :transaction_index], unique: true
t.index :block_number
t.index :block_timestamp
t.index :from_address
t.index :status
t.index :to_address
t.index :transaction_hash, unique: true
t.index :logs, using: :gin

t.check_constraint "block_number <= 4370000 AND status IS NULL OR block_number > 4370000 AND status = 1", name: "status_check"
t.check_constraint "created_contract_address IS NULL AND to_address IS NOT NULL OR
created_contract_address IS NOT NULL AND to_address IS NULL", name: "contract_to_check"

t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "from_address ~ '^0x[a-f0-9]{40}$'"
t.check_constraint "to_address IS NULL OR to_address ~ '^0x[a-f0-9]{40}$'"
t.check_constraint "created_contract_address IS NULL OR created_contract_address ~ '^0x[a-f0-9]{40}$'"
t.check_constraint "to_address ~ '^0x[a-f0-9]{40}$'"
t.check_constraint "created_contract_address ~ '^0x[a-f0-9]{40}$'"

t.foreign_key :eth_blocks, column: :block_number, primary_key: :block_number, on_delete: :cascade

Expand Down
23 changes: 11 additions & 12 deletions db/migrate/20231216164707_create_ethscriptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ def change
t.bigint :block_timestamp, null: false
t.bigint :event_log_index

t.bigint :ethscription_number#, null: false
t.bigint :ethscription_number, null: false
t.string :creator, null: false
t.string :initial_owner, null: false
t.string :current_owner, null: false
t.string :previous_owner, null: false

# t.boolean :valid_data_uri, null: false
t.text :content_uri, null: false
t.string :content_sha, null: false
# t.boolean :content_unique
t.boolean :esip6, null: false
t.string :mimetype, null: false
t.string :media_type, null: false
Expand All @@ -35,20 +33,17 @@ def change
t.index :creator
t.index :current_owner
t.index :ethscription_number, unique: true
# t.index [:content_unique, :valid_data_uri]
# t.index :content_unique, where: "(content_unique IS NOT NULL)"
t.index :content_sha
t.index :content_sha, unique: true, where: "(esip6 = false)",
name: :index_ethscriptions_on_content_sha_unique
# t.index :valid_data_uri
t.index :initial_owner
t.index :media_type
t.index :mime_subtype
t.index :mimetype
t.index :previous_owner
t.index :transaction_index
t.index :esip6

# t.check_constraint "esip6 = true OR content_unique IS NOT NULL"
t.check_constraint "content_sha ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
t.check_constraint "creator ~ '^0x[a-f0-9]{40}$'"
Expand All @@ -65,27 +60,31 @@ def change
reversible do |dir|
dir.up do
execute <<-SQL
CREATE OR REPLACE FUNCTION check_ethscription_order()
DROP TRIGGER IF EXISTS trigger_check_ethscription_order ON ethscriptions;
DROP FUNCTION IF EXISTS check_ethscription_order();

CREATE OR REPLACE FUNCTION check_ethscription_order_and_sequence()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.block_number < (SELECT MAX(block_number) FROM ethscriptions) OR
(NEW.block_number = (SELECT MAX(block_number) FROM ethscriptions) AND NEW.transaction_index <= (SELECT MAX(transaction_index) FROM ethscriptions WHERE block_number = NEW.block_number)) THEN
RAISE EXCEPTION 'Ethscriptions must be created in order';
END IF;
NEW.ethscription_number := (SELECT COALESCE(MAX(ethscription_number), -1) + 1 FROM ethscriptions);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_check_ethscription_order
CREATE TRIGGER trigger_check_ethscription_order_and_sequence
BEFORE INSERT ON ethscriptions
FOR EACH ROW EXECUTE FUNCTION check_ethscription_order();
FOR EACH ROW EXECUTE FUNCTION check_ethscription_order_and_sequence();
SQL
end

dir.down do
execute <<-SQL
DROP TRIGGER IF EXISTS trigger_check_ethscription_order ON ethscriptions;
DROP FUNCTION IF EXISTS check_ethscription_order();
DROP TRIGGER IF EXISTS trigger_check_ethscription_order_and_sequence ON ethscriptions;
DROP FUNCTION IF EXISTS check_ethscription_order_and_sequence();
SQL
end
end
Expand Down
Loading