Skip to content

Commit a59160b

Browse files
committed
State attestations
1 parent 45aa302 commit a59160b

10 files changed

Lines changed: 146 additions & 12 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: 8 additions & 4 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, 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

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: 27 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

@@ -20,6 +22,8 @@ def change
2022

2123
t.check_constraint "blockhash ~ '^0x[a-f0-9]{64}$'"
2224
t.check_constraint "parent_blockhash ~ '^0x[a-f0-9]{64}$'"
25+
t.check_constraint "state_hash ~ '^0x[a-f0-9]{64}$'"
26+
t.check_constraint "parent_state_hash ~ '^0x[a-f0-9]{64}$'"
2327

2428
t.timestamps
2529
end
@@ -49,6 +53,29 @@ def change
4953
FOR EACH ROW EXECUTE FUNCTION check_block_order();
5054
SQL
5155

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

db/migrate/20231216163233_create_eth_transactions.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def change
2929

3030
t.check_constraint "transaction_hash ~ '^0x[a-f0-9]{64}$'"
3131
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}$'"
32+
t.check_constraint "to_address ~ '^0x[a-f0-9]{40}$'"
33+
t.check_constraint "created_contract_address ~ '^0x[a-f0-9]{40}$'"
3434

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

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/20231216215348_create_ethscription_ownership_versions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def change
1313

1414
t.index :ethscription_transaction_hash
1515
t.index :transaction_hash
16+
t.index :block_number
1617
t.index [:transaction_hash, :transfer_index], unique: true
1718
t.index [:block_number, :transaction_index, :transfer_index], unique: true
1819
t.index [:ethscription_transaction_hash, :block_number, :transaction_index, :transfer_index], unique: true

db/structure.sql

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,34 @@ CREATE FUNCTION public.check_block_order() RETURNS trigger
6464
NEW.parent_blockhash <> (SELECT blockhash FROM eth_blocks WHERE block_number = NEW.block_number - 1) THEN
6565
RAISE EXCEPTION 'Parent block hash does not match the parent''s block hash';
6666
END IF;
67-
67+
6868
RETURN NEW;
6969
END;
7070
$$;
7171

7272

73+
--
74+
-- Name: check_block_order_on_update(); Type: FUNCTION; Schema: public; Owner: -
75+
--
76+
77+
CREATE FUNCTION public.check_block_order_on_update() RETURNS trigger
78+
LANGUAGE plpgsql
79+
AS $$
80+
BEGIN
81+
IF NEW.imported_at IS NOT NULL AND NEW.state_hash IS NULL THEN
82+
RAISE EXCEPTION 'state_hash must be set when imported_at is set';
83+
END IF;
84+
85+
IF NEW.is_genesis_block = false AND
86+
NEW.parent_state_hash <> (SELECT state_hash FROM eth_blocks WHERE block_number = NEW.block_number - 1 AND imported_at IS NOT NULL) THEN
87+
RAISE EXCEPTION 'Parent state hash does not match the state hash of the previous block';
88+
END IF;
89+
90+
RETURN NEW;
91+
END;
92+
$$;
93+
94+
7395
--
7496
-- Name: check_ethscription_order_and_sequence(); Type: FUNCTION; Schema: public; Owner: -
7597
--
@@ -208,10 +230,14 @@ CREATE TABLE public.eth_blocks (
208230
blockhash character varying NOT NULL,
209231
parent_blockhash character varying NOT NULL,
210232
imported_at timestamp(6) without time zone,
233+
state_hash character varying,
234+
parent_state_hash character varying,
211235
is_genesis_block boolean NOT NULL,
212236
created_at timestamp(6) without time zone NOT NULL,
213237
updated_at timestamp(6) without time zone NOT NULL,
214238
CONSTRAINT chk_rails_1c105acdac CHECK (((parent_blockhash)::text ~ '^0x[a-f0-9]{64}$'::text)),
239+
CONSTRAINT chk_rails_319237323b CHECK (((state_hash)::text ~ '^0x[a-f0-9]{64}$'::text)),
240+
CONSTRAINT chk_rails_7126b7c9d3 CHECK (((parent_state_hash)::text ~ '^0x[a-f0-9]{64}$'::text)),
215241
CONSTRAINT chk_rails_7e9881ece2 CHECK (((blockhash)::text ~ '^0x[a-f0-9]{64}$'::text))
216242
);
217243

@@ -257,10 +283,10 @@ CREATE TABLE public.eth_transactions (
257283
value numeric NOT NULL,
258284
created_at timestamp(6) without time zone NOT NULL,
259285
updated_at timestamp(6) without time zone NOT NULL,
260-
CONSTRAINT chk_rails_51be5c1aa9 CHECK (((to_address IS NULL) OR ((to_address)::text ~ '^0x[a-f0-9]{40}$'::text))),
261-
CONSTRAINT chk_rails_93b41d08e7 CHECK (((created_contract_address IS NULL) OR ((created_contract_address)::text ~ '^0x[a-f0-9]{40}$'::text))),
286+
CONSTRAINT chk_rails_37ed5d6017 CHECK (((to_address)::text ~ '^0x[a-f0-9]{40}$'::text)),
262287
CONSTRAINT chk_rails_9cdbd3b1ad CHECK (((transaction_hash)::text ~ '^0x[a-f0-9]{64}$'::text)),
263288
CONSTRAINT chk_rails_a4d3f41974 CHECK (((from_address)::text ~ '^0x[a-f0-9]{40}$'::text)),
289+
CONSTRAINT chk_rails_d460e80110 CHECK (((created_contract_address)::text ~ '^0x[a-f0-9]{40}$'::text)),
264290
CONSTRAINT contract_to_check CHECK ((((created_contract_address IS NULL) AND (to_address IS NOT NULL)) OR ((created_contract_address IS NOT NULL) AND (to_address IS NULL)))),
265291
CONSTRAINT status_check CHECK ((((block_number <= 4370000) AND (status IS NULL)) OR ((block_number > 4370000) AND (status = 1))))
266292
);
@@ -400,10 +426,10 @@ CREATE TABLE public.ethscriptions (
400426
created_at timestamp(6) without time zone NOT NULL,
401427
updated_at timestamp(6) without time zone NOT NULL,
402428
CONSTRAINT chk_rails_52497428f2 CHECK (((previous_owner)::text ~ '^0x[a-f0-9]{40}$'::text)),
429+
CONSTRAINT chk_rails_528fcbfbaa CHECK (((content_sha)::text ~ '^0x[a-f0-9]{64}$'::text)),
403430
CONSTRAINT chk_rails_6f8922831e CHECK (((current_owner)::text ~ '^0x[a-f0-9]{40}$'::text)),
404431
CONSTRAINT chk_rails_84591e2730 CHECK (((transaction_hash)::text ~ '^0x[a-f0-9]{64}$'::text)),
405432
CONSTRAINT chk_rails_b577b97822 CHECK (((creator)::text ~ '^0x[a-f0-9]{40}$'::text)),
406-
CONSTRAINT chk_rails_d741e3044d CHECK (((content_sha)::text ~ '^[a-f0-9]{64}$'::text)),
407433
CONSTRAINT chk_rails_df21fdbe02 CHECK (((initial_owner)::text ~ '^0x[a-f0-9]{40}$'::text))
408434
);
409435

@@ -696,6 +722,13 @@ CREATE INDEX index_eth_transactions_on_to_address ON public.eth_transactions USI
696722
CREATE UNIQUE INDEX index_eth_transactions_on_transaction_hash ON public.eth_transactions USING btree (transaction_hash);
697723

698724

725+
--
726+
-- Name: index_ethscription_ownership_versions_on_block_number; Type: INDEX; Schema: public; Owner: -
727+
--
728+
729+
CREATE INDEX index_ethscription_ownership_versions_on_block_number ON public.ethscription_ownership_versions USING btree (block_number);
730+
731+
699732
--
700733
-- Name: index_ethscription_ownership_versions_on_transaction_hash; Type: INDEX; Schema: public; Owner: -
701734
--
@@ -857,6 +890,13 @@ CREATE TRIGGER check_block_imported_at_trigger BEFORE UPDATE OF imported_at ON p
857890
CREATE TRIGGER trigger_check_block_order BEFORE INSERT ON public.eth_blocks FOR EACH ROW EXECUTE FUNCTION public.check_block_order();
858891

859892

893+
--
894+
-- Name: eth_blocks trigger_check_block_order_on_update; Type: TRIGGER; Schema: public; Owner: -
895+
--
896+
897+
CREATE TRIGGER trigger_check_block_order_on_update BEFORE UPDATE OF imported_at ON public.eth_blocks FOR EACH ROW WHEN ((new.imported_at IS NOT NULL)) EXECUTE FUNCTION public.check_block_order_on_update();
898+
899+
860900
--
861901
-- Name: ethscriptions trigger_check_ethscription_order_and_sequence; Type: TRIGGER; Schema: public; Owner: -
862902
--

0 commit comments

Comments
 (0)