Skip to content

Commit 15843ec

Browse files
Merge pull request #1 from 0xFacet/rc1
RC1
2 parents 3236d3a + 8de4c49 commit 15843ec

14 files changed

Lines changed: 457 additions & 130 deletions

app/models/eth_block.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ class BlockNotReadyToImportError < StandardError; end
77
inverse_of: :eth_block
88
has_many :ethscription_transfers, foreign_key: :block_number, primary_key: :block_number,
99
inverse_of: :eth_block
10+
has_many :ethscription_ownership_versions, foreign_key: :block_number, primary_key: :block_number,
11+
inverse_of: :eth_block
1012

13+
before_validation :generate_attestation_hash, if: -> { imported_at.present? }
14+
1115
def self.genesis_blocks
1216
blocks = if ENV.fetch('ETHEREUM_NETWORK') == "eth-mainnet"
1317
[1608625, 3369985, 3981254, 5873780, 8205613, 9046950,
@@ -236,4 +240,46 @@ def as_json(options = {})
236240
)
237241
).with_indifferent_access
238242
end
243+
244+
def generate_attestation_hash
245+
hash = Digest::SHA256.new
246+
247+
parent_state_hash = EthBlock.where(block_number: block_number - 1).
248+
limit(1).pluck(:state_hash).first
249+
250+
hash << (parent_state_hash || "NULL")
251+
hash << hashable_attributes(self.class).map { |attr| send(attr) }.
252+
map { |record| record.nil? ? 'NULL' : record }.join
253+
254+
associations_to_hash.each do |association|
255+
hashable_attributes = quoted_hashable_attributes(association.klass)
256+
records = association_scope(association).pluck(*hashable_attributes)
257+
258+
records.map! { |record| record.nil? ? 'NULL' : record }
259+
hash << records.join
260+
end
261+
262+
self.state_hash = "0x" + hash.hexdigest
263+
self.parent_state_hash = parent_state_hash
264+
end
265+
266+
def association_scope(association)
267+
association.klass.oldest_first.where(block_number: block_number)
268+
end
269+
270+
def associations_to_hash
271+
self.class.reflect_on_all_associations(:has_many)
272+
end
273+
274+
def hashable_attributes(klass)
275+
klass.columns_hash.reject do |k, v|
276+
v.type == :datetime || ['id'].include?(k)
277+
end.keys.sort
278+
end
279+
280+
def quoted_hashable_attributes(klass)
281+
hashable_attributes(klass).map do |attr|
282+
Arel.sql("encode(digest(#{klass.connection.quote_column_name(attr)}::text, 'sha256'), 'hex')")
283+
end
284+
end
239285
end

app/models/eth_transaction.rb

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11

22
class EthTransaction < ApplicationRecord
33
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number, optional: true,
4-
inverse_of: :eth_transaction
4+
inverse_of: :eth_block
55
has_one :ethscription, foreign_key: :transaction_hash, primary_key: :transaction_hash,
6-
inverse_of: :eth_transaction, dependent: :destroy
7-
6+
inverse_of: :eth_transaction
87
has_many :ethscription_transfers, foreign_key: :transaction_hash,
9-
primary_key: :transaction_hash, dependent: :destroy, inverse_of: :eth_transaction
8+
primary_key: :transaction_hash, inverse_of: :eth_transaction
9+
has_many :ethscription_ownership_versions, foreign_key: :transaction_hash,
10+
primary_key: :transaction_hash, inverse_of: :eth_transaction
1011

1112
attr_accessor :transfer_index
1213

14+
scope :newest_first, -> { order(block_number: :desc, transaction_index: :desc) }
15+
scope :oldest_first, -> { order(block_number: :asc, transaction_index: :asc) }
16+
17+
def self.event_signature(event_name)
18+
"0x" + Digest::Keccak256.hexdigest(event_name)
19+
end
20+
21+
CreateEthscriptionEventSig = event_signature("ethscriptions_protocol_CreateEthscription(address,string)")
22+
Esip2EventSig = event_signature("ethscriptions_protocol_TransferEthscriptionForPreviousOwner(address,address,bytes32)")
23+
Esip1EventSig = event_signature("ethscriptions_protocol_TransferEthscription(address,bytes32)")
24+
1325
def possibly_relevant?
1426
status != 0 &&
1527
(possibly_creates_ethscription? || possibly_transfers_ethscription?)
@@ -104,7 +116,7 @@ def ethscription_creation_events
104116
return [] unless EthTransaction.esip3_enabled?(block_number)
105117

106118
ordered_events.select do |log|
107-
EthTransaction.contracts_create_ethscription_event_sig == log['topics'].first
119+
CreateEthscriptionEventSig == log['topics'].first
108120
end
109121
end
110122

@@ -143,16 +155,17 @@ def create_ethscription_transfers_from_events!
143155
topics = log['topics']
144156
event_type = topics.first
145157

146-
if event_type == EthTransaction.esip1_transfer_event_signature
158+
if event_type == Esip1EventSig
147159
begin
148-
event_to = Eth::Abi.decode(['address'], topics.third).first
160+
event_to = Eth::Abi.decode(['address'], topics.second).first
161+
tx_hash = Eth::Util.bin_to_prefixed_hex(
162+
Eth::Abi.decode(['bytes32'], topics.third).first
163+
)
149164
rescue Eth::Abi::DecodingError
150165
next
151166
end
152167

153-
next unless valid_bytes32?(topics.third)
154-
155-
target_ethscription = Ethscription.find_by(transaction_hash: topics.third)
168+
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)
156169

157170
if target_ethscription.present?
158171
ethscription_transfers.create!(
@@ -165,17 +178,18 @@ def create_ethscription_transfers_from_events!
165178
}.merge(transfer_attrs)
166179
)
167180
end
168-
elsif event_type == EthTransaction.esip2_transfer_event_signature
181+
elsif event_type == Esip2EventSig
169182
begin
170183
event_previous_owner = Eth::Abi.decode(['address'], topics.second).first
171184
event_to = Eth::Abi.decode(['address'], topics.third).first
185+
tx_hash = Eth::Util.bin_to_prefixed_hex(
186+
Eth::Abi.decode(['bytes32'], topics.fourth).first
187+
)
172188
rescue Eth::Abi::DecodingError
173189
next
174190
end
175191

176-
next unless valid_bytes32?(topics.fourth)
177-
178-
target_ethscription = Ethscription.find_by(transaction_hash: topics.fourth)
192+
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)
179193

180194
if target_ethscription.present?
181195
ethscription_transfers.create!(
@@ -277,40 +291,11 @@ def self.esip1_enabled?(block_number)
277291
block_number >= 17672762
278292
end
279293

280-
def valid_bytes32?(value)
281-
/\A0x[0-9a-f]{64}\z/i.match?(value.to_s)
282-
end
283-
284294
def self.contract_transfer_event_signatures(block_number)
285295
[].tap do |res|
286-
res << esip1_transfer_event_signature if esip1_enabled?(block_number)
287-
res << esip2_transfer_event_signature if esip2_enabled?(block_number)
288-
end
289-
end
290-
291-
class << self
292-
extend Memoist
293-
294-
def contracts_create_ethscription_event_sig
295-
"0x" + Digest::Keccak256.hexdigest(
296-
"ethscriptions_protocol_CreateEthscription(address,string)"
297-
)
298-
end
299-
memoize :contracts_create_ethscription_event_sig
300-
301-
def esip2_transfer_event_signature
302-
"0x" + Digest::Keccak256.hexdigest(
303-
"ethscriptions_protocol_TransferEthscriptionForPreviousOwner(address,address,bytes32)"
304-
)
305-
end
306-
memoize :esip2_transfer_event_signature
307-
308-
def esip1_transfer_event_signature
309-
"0x" + Digest::Keccak256.hexdigest(
310-
"ethscriptions_protocol_TransferEthscription(address,bytes32)"
311-
)
296+
res << Esip1EventSig if esip1_enabled?(block_number)
297+
res << Esip2EventSig if esip2_enabled?(block_number)
312298
end
313-
memoize :esip1_transfer_event_signature
314299
end
315300

316301
def self.prune_transactions

app/models/ethscription_ownership_version.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class EthscriptionOwnershipVersion < ApplicationRecord
2-
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number, optional: true,
3-
inverse_of: :ethscription_ownership_versions
2+
belongs_to :eth_block, foreign_key: :block_number, primary_key: :block_number,
3+
optional: true, inverse_of: :ethscription_ownership_versions
44
belongs_to :eth_transaction,
55
foreign_key: :transaction_hash,
66
primary_key: :transaction_hash, optional: true,
@@ -15,4 +15,10 @@ class EthscriptionOwnershipVersion < ApplicationRecord
1515
transaction_index: :desc,
1616
transfer_index: :desc
1717
)}
18+
19+
scope :oldest_first, -> { order(
20+
block_number: :asc,
21+
transaction_index: :asc,
22+
transfer_index: :asc
23+
)}
1824
end

app/models/ethscription_transfer.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ class EthscriptionTransfer < ApplicationRecord
88

99
after_create :create_ownership_version!, :notify_eth_transaction
1010

11+
scope :newest_first, -> { order(
12+
block_number: :desc,
13+
transaction_index: :desc,
14+
transfer_index: :desc
15+
)}
16+
17+
scope :oldest_first, -> { order(
18+
block_number: :asc,
19+
transaction_index: :asc,
20+
transfer_index: :asc
21+
)}
22+
1123
def notify_eth_transaction
1224
if eth_transaction.transfer_index.nil?
1325
raise "Need eth_transaction.transfer_index"

db/migrate/20231216161930_create_eth_blocks.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
class CreateEthBlocks < ActiveRecord::Migration[7.1]
22
def change
3+
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
4+
35
create_table :eth_blocks, force: :cascade do |t|
46
t.bigint :block_number, null: false
57
t.bigint :timestamp, null: false
68
t.string :blockhash, null: false
79
t.string :parent_blockhash, null: false
810
t.datetime :imported_at
11+
t.string :state_hash
12+
t.string :parent_state_hash
913

1014
t.boolean :is_genesis_block, null: false
1115

1216
t.index :block_number, unique: true
1317
t.index :blockhash, unique: true
1418
t.index :imported_at
19+
t.index [:imported_at, :block_number]
1520
t.index :parent_blockhash, unique: true
21+
t.index :state_hash, unique: true
22+
t.index :parent_state_hash, unique: true
1623
t.index :timestamp
1724

1825
t.check_constraint "blockhash ~ '^0x[a-f0-9]{64}$'"
1926
t.check_constraint "parent_blockhash ~ '^0x[a-f0-9]{64}$'"
27+
t.check_constraint "state_hash ~ '^0x[a-f0-9]{64}$'"
28+
t.check_constraint "parent_state_hash ~ '^0x[a-f0-9]{64}$'"
2029

2130
t.timestamps
2231
end
@@ -46,6 +55,29 @@ def change
4655
FOR EACH ROW EXECUTE FUNCTION check_block_order();
4756
SQL
4857

58+
execute <<~SQL
59+
CREATE OR REPLACE FUNCTION check_block_order_on_update()
60+
RETURNS TRIGGER AS $$
61+
BEGIN
62+
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
63+
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
64+
END IF;
65+
66+
IF NEW.is_genesis_block = false AND
67+
NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = NEW.block_number - 1 AND imported_at IS NOT NULL) THEN
68+
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
69+
END IF;
70+
71+
RETURN NEW;
72+
END;
73+
$$ LANGUAGE plpgsql;
74+
75+
CREATE TRIGGER trigger_check_block_order_on_update
76+
BEFORE UPDATE OF imported_at ON eth_blocks
77+
FOR EACH ROW WHEN (NEW.imported_at IS NOT NULL)
78+
EXECUTE FUNCTION check_block_order_on_update();
79+
SQL
80+
4981
execute <<-SQL
5082
CREATE OR REPLACE FUNCTION delete_later_blocks()
5183
RETURNS TRIGGER AS $$

db/migrate/20231216163233_create_eth_transactions.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,21 @@ def change
1818

1919
t.index [:block_number, :transaction_index], unique: true
2020
t.index :block_number
21+
t.index :block_timestamp
2122
t.index :from_address
2223
t.index :status
2324
t.index :to_address
2425
t.index :transaction_hash, unique: true
26+
t.index :logs, using: :gin
2527

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

3032
t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
3133
t.check_constraint "from_address ~ '^0x[a-f0-9]{40}$'"
32-
t.check_constraint "to_address IS NULL OR to_address ~ '^0x[a-f0-9]{40}$'"
33-
t.check_constraint "created_contract_address IS NULL OR created_contract_address ~ '^0x[a-f0-9]{40}$'"
34+
t.check_constraint "to_address ~ '^0x[a-f0-9]{40}$'"
35+
t.check_constraint "created_contract_address ~ '^0x[a-f0-9]{40}$'"
3436

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

db/migrate/20231216164707_create_ethscriptions.rb

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ def change
77
t.bigint :block_timestamp, null: false
88
t.bigint :event_log_index
99

10-
t.bigint :ethscription_number#, null: false
10+
t.bigint :ethscription_number, null: false
1111
t.string :creator, null: false
1212
t.string :initial_owner, null: false
1313
t.string :current_owner, null: false
1414
t.string :previous_owner, null: false
1515

16-
# t.boolean :valid_data_uri, null: false
1716
t.text :content_uri, null: false
1817
t.string :content_sha, null: false
19-
# t.boolean :content_unique
2018
t.boolean :esip6, null: false
2119
t.string :mimetype, null: false
2220
t.string :media_type, null: false
@@ -35,20 +33,17 @@ def change
3533
t.index :creator
3634
t.index :current_owner
3735
t.index :ethscription_number, unique: true
38-
# t.index [:content_unique, :valid_data_uri]
39-
# t.index :content_unique, where: "(content_unique IS NOT NULL)"
4036
t.index :content_sha
4137
t.index :content_sha, unique: true, where: "(esip6 = false)",
4238
name: :index_ethscriptions_on_content_sha_unique
43-
# t.index :valid_data_uri
4439
t.index :initial_owner
4540
t.index :media_type
4641
t.index :mime_subtype
4742
t.index :mimetype
4843
t.index :previous_owner
4944
t.index :transaction_index
45+
t.index :esip6
5046

51-
# t.check_constraint "esip6 = true OR content_unique IS NOT NULL"
5247
t.check_constraint "content_sha ~ '^0x[a-f0-9]{64}$'"
5348
t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
5449
t.check_constraint "creator ~ '^0x[a-f0-9]{40}$'"
@@ -65,27 +60,31 @@ def change
6560
reversible do |dir|
6661
dir.up do
6762
execute <<-SQL
68-
CREATE OR REPLACE FUNCTION check_ethscription_order()
63+
DROP TRIGGER IF EXISTS trigger_check_ethscription_order ON ethscriptions;
64+
DROP FUNCTION IF EXISTS check_ethscription_order();
65+
66+
CREATE OR REPLACE FUNCTION check_ethscription_order_and_sequence()
6967
RETURNS TRIGGER AS $$
7068
BEGIN
7169
IF NEW.block_number < (SELECT MAX(block_number) FROM ethscriptions) OR
7270
(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
7371
RAISE EXCEPTION 'Ethscriptions must be created in order';
7472
END IF;
73+
NEW.ethscription_number := (SELECT COALESCE(MAX(ethscription_number), -1) + 1 FROM ethscriptions);
7574
RETURN NEW;
7675
END;
7776
$$ LANGUAGE plpgsql;
7877
79-
CREATE TRIGGER trigger_check_ethscription_order
78+
CREATE TRIGGER trigger_check_ethscription_order_and_sequence
8079
BEFORE INSERT ON ethscriptions
81-
FOR EACH ROW EXECUTE FUNCTION check_ethscription_order();
80+
FOR EACH ROW EXECUTE FUNCTION check_ethscription_order_and_sequence();
8281
SQL
8382
end
8483

8584
dir.down do
8685
execute <<-SQL
87-
DROP TRIGGER IF EXISTS trigger_check_ethscription_order ON ethscriptions;
88-
DROP FUNCTION IF EXISTS check_ethscription_order();
86+
DROP TRIGGER IF EXISTS trigger_check_ethscription_order_and_sequence ON ethscriptions;
87+
DROP FUNCTION IF EXISTS check_ethscription_order_and_sequence();
8988
SQL
9089
end
9190
end

0 commit comments

Comments
 (0)