Skip to content

Commit 8de4c49

Browse files
Merge pull request #2 from 0xFacet/state_attestations
State attestations
2 parents 45aa302 + dda45ff commit 8de4c49

13 files changed

Lines changed: 398 additions & 26 deletions

app/models/eth_block.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class BlockNotReadyToImportError < StandardError; end
1010
has_many :ethscription_ownership_versions, foreign_key: :block_number, primary_key: :block_number,
1111
inverse_of: :eth_block
1212

13+
before_validation :generate_attestation_hash, if: -> { imported_at.present? }
14+
1315
def self.genesis_blocks
1416
blocks = if ENV.fetch('ETHEREUM_NETWORK') == "eth-mainnet"
1517
[1608625, 3369985, 3981254, 5873780, 8205613, 9046950,
@@ -238,4 +240,46 @@ def as_json(options = {})
238240
)
239241
).with_indifferent_access
240242
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
241285
end

app/models/eth_transaction.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
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+
1317
def self.event_signature(event_name)
1418
"0x" + Digest::Keccak256.hexdigest(event_name)
1519
end
@@ -153,14 +157,15 @@ def create_ethscription_transfers_from_events!
153157

154158
if event_type == Esip1EventSig
155159
begin
156-
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+
)
157164
rescue Eth::Abi::DecodingError
158165
next
159166
end
160167

161-
next unless valid_bytes32?(topics.third)
162-
163-
target_ethscription = Ethscription.find_by(transaction_hash: topics.third)
168+
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)
164169

165170
if target_ethscription.present?
166171
ethscription_transfers.create!(
@@ -177,13 +182,14 @@ def create_ethscription_transfers_from_events!
177182
begin
178183
event_previous_owner = Eth::Abi.decode(['address'], topics.second).first
179184
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+
)
180188
rescue Eth::Abi::DecodingError
181189
next
182190
end
183191

184-
next unless valid_bytes32?(topics.fourth)
185-
186-
target_ethscription = Ethscription.find_by(transaction_hash: topics.fourth)
192+
target_ethscription = Ethscription.find_by(transaction_hash: tx_hash)
187193

188194
if target_ethscription.present?
189195
ethscription_transfers.create!(
@@ -285,10 +291,6 @@ def self.esip1_enabled?(block_number)
285291
block_number >= 17672762
286292
end
287293

288-
def valid_bytes32?(value)
289-
/\A0x[0-9a-f]{64}\z/i.match?(value.to_s)
290-
end
291-
292294
def self.contract_transfer_event_signatures(block_number)
293295
[].tap do |res|
294296
res << Esip1EventSig if esip1_enabled?(block_number)

app/models/ethscription.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def parsed_data_uri
4646
end
4747

4848
def content_sha
49-
Digest::SHA256.hexdigest(content_uri)
49+
"0x" + Digest::SHA256.hexdigest(content_uri)
5050
end
5151

5252
def esip6

app/models/ethscription_ownership_version.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ def change
88
t.string :blockhash, null: false
99
t.string :parent_blockhash, null: false
1010
t.datetime :imported_at
11+
t.string :state_hash
12+
t.string :parent_state_hash
1113

1214
t.boolean :is_genesis_block, null: false
1315

@@ -16,10 +18,14 @@ def change
1618
t.index :imported_at
1719
t.index [:imported_at, :block_number]
1820
t.index :parent_blockhash, unique: true
21+
t.index :state_hash, unique: true
22+
t.index :parent_state_hash, unique: true
1923
t.index :timestamp
2024

2125
t.check_constraint "blockhash ~ '^0x[a-f0-9]{64}$'"
2226
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}$'"
2329

2430
t.timestamps
2531
end
@@ -49,6 +55,29 @@ def change
4955
FOR EACH ROW EXECUTE FUNCTION check_block_order();
5056
SQL
5157

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+
5281
execute <<-SQL
5382
CREATE OR REPLACE FUNCTION delete_later_blocks()
5483
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def change
4444
t.index :transaction_index
4545
t.index :esip6
4646

47-
t.check_constraint "content_sha ~ '^[a-f0-9]{64}$'"
47+
t.check_constraint "content_sha ~ '^0x[a-f0-9]{64}$'"
4848
t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
4949
t.check_constraint "creator ~ '^0x[a-f0-9]{40}$'"
5050
t.check_constraint "current_owner ~ '^0x[a-f0-9]{40}$'"

db/migrate/20231216213103_create_ethscription_transfers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def change
1212
t.bigint :transaction_index, null: false
1313
t.string :enforced_previous_owner
1414

15+
t.index :ethscription_transaction_hash
1516
t.index :block_number
1617
t.index :from_address
1718
t.index :to_address

db/migrate/20231216215348_create_ethscription_ownership_versions.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ def change
1111
t.string :current_owner, null: false
1212
t.string :previous_owner, null: false
1313

14+
t.index :current_owner
15+
t.index :previous_owner
16+
t.index [:current_owner, :previous_owner]
1417
t.index :ethscription_transaction_hash
1518
t.index :transaction_hash
19+
t.index :block_number
1620
t.index [:transaction_hash, :transfer_index], unique: true
1721
t.index [:block_number, :transaction_index, :transfer_index], unique: true
1822
t.index [:ethscription_transaction_hash, :block_number, :transaction_index, :transfer_index], unique: true

0 commit comments

Comments
 (0)