From 5384bb0b00f6c96930677c4a7f55631639c4e86e Mon Sep 17 00:00:00 2001 From: Brandon Kase Date: Wed, 10 Nov 2021 01:06:59 +0000 Subject: [PATCH 1/2] Rosetta: Fast canonical lookups chain_status The rosetta side of the equation --- src/app/rosetta/lib/account.ml | 154 +++++++++++++-------------------- src/app/rosetta/lib/block.ml | 48 +++++++++- 2 files changed, 106 insertions(+), 96 deletions(-) diff --git a/src/app/rosetta/lib/account.ml b/src/app/rosetta/lib/account.ml index 5f13180f755..dcc8c202f79 100644 --- a/src/app/rosetta/lib/account.ml +++ b/src/app/rosetta/lib/account.ml @@ -31,25 +31,8 @@ end module Sql = struct module Balance_from_last_relevant_command = struct - let query = - Caqti_request.find_opt - Caqti_type.(tup2 string int64) - Caqti_type.(tup2 int64 int64) - {sql| -WITH RECURSIVE chain AS ( - (SELECT id, state_hash, parent_id, height, global_slot_since_genesis, timestamp - FROM blocks b - WHERE height = (select MAX(height) from blocks) - ORDER BY timestamp ASC - LIMIT 1) - - UNION ALL - - SELECT b.id, b.state_hash, b.parent_id, b.height, b.global_slot_since_genesis, b.timestamp FROM blocks b - INNER JOIN chain - ON b.id = chain.parent_id AND chain.id <> chain.parent_id -), - + let common_sql = + {sql| relevant_internal_block_balances AS ( SELECT block_internal_command.block_id, @@ -114,6 +97,28 @@ relevant_block_balances AS ( UNION (SELECT block_id, sequence_no, 0 AS secondary_sequence_no, balance FROM relevant_user_block_balances) ) + |sql} + + let query_recent = + Caqti_request.find_opt + Caqti_type.(tup2 string int64) + Caqti_type.(tup2 int64 int64) + (sprintf {sql| +WITH RECURSIVE chain AS ( + (SELECT id, state_hash, parent_id, height, global_slot_since_genesis, timestamp + FROM blocks b + WHERE height = (select MAX(height) from blocks) + ORDER BY timestamp ASC + LIMIT 1) + + UNION ALL + + SELECT b.id, b.state_hash, b.parent_id, b.height, b.global_slot_since_genesis, b.timestamp FROM blocks b + INNER JOIN chain + ON b.id = chain.parent_id AND chain.id <> chain.parent_id +), + +%s SELECT chain.global_slot_since_genesis AS block_global_slot_since_genesis, @@ -124,11 +129,35 @@ JOIN relevant_block_balances rbb ON chain.id = rbb.block_id WHERE chain.height <= $2 ORDER BY (chain.height, sequence_no, secondary_sequence_no) DESC LIMIT 1 - |sql} + |sql} common_sql) + + let query_old = + Caqti_request.find_opt + Caqti_type.(tup2 string int64) + Caqti_type.(tup2 int64 int64) +(sprintf {sql| +%s + +SELECT + b.global_slot_since_genesis AS block_global_slot_since_genesis, + balance +FROM +blocks b +JOIN relevant_block_balances rbb ON b.id = rbb.block_id +WHERE b.height <= $2 AND b.chain_status = 'canonical' +ORDER BY (chain.height, sequence_no, secondary_sequence_no) DESC +LIMIT 1 + |sql} common_sql) let run (module Conn : Caqti_async.CONNECTION) requested_block_height address = - Conn.find_opt query (address, requested_block_height) + let open Deferred.Result.Let_syntax in + (* buffer an epsilon of 5 just in case archive node isn't caught up *) + let%bind is_old_height = Sql.Block.run_is_old_height (module Conn) ~height:requested_block_height in + if is_old_height then + Conn.find_opt query_old (address, requested_block_height) + else + Conn.find_opt query_recent (address, requested_block_height) end let run (module Conn : Caqti_async.CONNECTION) block_query address = @@ -387,79 +416,18 @@ module Balance = struct in {amount with metadata= Some metadata} in - let find_via_db ~block_query ~address = - let%map block_identifier, {liquid_balance; total_balance} = - env.db_block_identifier_and_balance_info ~block_query ~address - in - { Account_balance_response.block_identifier - ; balances= - [ make_balance_amount - ~liquid_balance:(Unsigned.UInt64.of_int64 liquid_balance) - ~total_balance:(Unsigned.UInt64.of_int64 total_balance) ] - ; metadata=Some (`Assoc [ ("created_via_historical_lookup", `Bool true ) ]) } + let%bind block_query = + Query.of_partial_identifier' req.block_identifier in - match req.block_identifier with - | None -> - (* First try via GraphQL but fallback to archive database (and then - * omit the nonce!) if there was an issue *) - (match%bind - env.gql - ?token_id:(Option.map token_id ~f:Unsigned.UInt64.to_string) - ~address () - with - | `Failed e -> - [%log' warn env.logger] "/account/balance : GraphQL request failed, trying again via the archive database" ~metadata:[("error", Errors.erase e |> Rosetta_models.Error.to_yojson)]; - find_via_db ~block_query:None ~address - | `Successful res -> - let%bind account = - match res#account with - | None -> - M.fail (Errors.create (`Account_not_found address)) - | Some account -> - M.return account - in - let%bind state_hash = - match (account#balance)#stateHash with - | None -> - M.fail - (Errors.create - ~context: - "Failed accessing state hash from GraphQL \ - communication with the Mina Daemon." - `Chain_info_missing) - | Some state_hash -> - M.return state_hash - in - let%map liquid_balance = - match (account#balance)#liquid with - | None -> - M.fail - (Errors.create - ~context: - "Unable to access liquid balance since your Mina \ - daemon isn't fully bootstrapped." - `Chain_info_missing) - | Some liquid_balance -> - M.return liquid_balance - in - let metadata = - Option.map - ~f:(fun nonce -> `Assoc [("nonce", `Intlit nonce)]) - account#nonce - in - let total_balance = (account#balance)#total in - { Account_balance_response.block_identifier= - { Block_identifier.index= - Unsigned.UInt32.to_int64 (account#balance)#blockHeight - ; hash= state_hash } - ; balances= [make_balance_amount ~liquid_balance ~total_balance] - ; metadata }) - | Some partial_identifier -> - (* TODO: Once multiple token_ids are possible we may need to add handling for that here *) - let%bind block_query = - Query.of_partial_identifier partial_identifier - in - find_via_db ~block_query ~address + let%map block_identifier, {liquid_balance; total_balance} = + env.db_block_identifier_and_balance_info ~block_query ~address + in + { Account_balance_response.block_identifier + ; balances= + [ make_balance_amount + ~liquid_balance:(Unsigned.UInt64.of_int64 liquid_balance) + ~total_balance:(Unsigned.UInt64.of_int64 total_balance) ] + ; metadata=Some (`Assoc [ ("created_via_historical_lookup", `Bool true ) ]) } end module Real = Impl (Deferred.Result) diff --git a/src/app/rosetta/lib/block.ml b/src/app/rosetta/lib/block.ml index 9ea375198e6..02629a05546 100644 --- a/src/app/rosetta/lib/block.ml +++ b/src/app/rosetta/lib/block.ml @@ -45,6 +45,9 @@ module Block_query = struct | Some index, Some hash -> M.return (Some (`Those (`Height index, `Hash hash))) + let of_partial_identifier' (identifier : Partial_block_identifier.t option) = + of_partial_identifier (Option.value identifier ~default:{Partial_block_identifier.index = None; hash = None }) + let is_genesis ~hash = function | Some (`This (`Height index)) -> Int64.equal index Network.genesis_block_height @@ -241,7 +244,26 @@ module Sql = struct let typ = Caqti_type.(tup3 int Archive_lib.Processor.Block.typ Extras.typ) - let query_height = + let query_max_height = + Caqti_request.find Caqti_type.unit Caqti_type.int64 + {| SELECT MAX(height) FROM blocks |} + + let query_height_old = + Caqti_request.find_opt Caqti_type.int64 typ + (* The archive database will only reconcile the canonical columns for + * blocks older than k + epsilon + *) + {| +SELECT c.id, c.state_hash, c.parent_id, c.parent_hash, c.creator_id, c.block_winner_id, c.snarked_ledger_hash_id, c.staking_epoch_data_id, c.next_epoch_data_id, c.ledger_hash, c.height, c.global_slot, c.global_slot_since_genesis, c.timestamp, pk.value as creator, bw.value as winner FROM blocks c + INNER JOIN public_keys pk + ON pk.id = c.creator_id + INNER JOIN public_keys bw + ON bw.id = c.block_winner_id + WHERE c.height = ? AND c.chain_status = 'canonical' + |} + + + let query_height_recent = Caqti_request.find_opt Caqti_type.int64 typ (* According to the clarification of the Rosetta spec here * https://community.rosetta-api.org/t/querying-block-by-just-its-index/84/3 , @@ -249,7 +271,12 @@ module Sql = struct * given height, and not an arbitrary one. * * This query recursively traverses the blockchain from the longest tip - * backwards until it reaches a block of the given height. *) + * backwards until it reaches a block of the given height. + * + * This query is best used only for _short_ (around ~k blocks back + * + epsilon) + * requests since recursive queries stress PostgreSQL. + *) {| WITH RECURSIVE chain AS ( (SELECT id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, ledger_hash, height, global_slot, global_slot_since_genesis, timestamp FROM blocks b WHERE height = (select MAX(height) from blocks) @@ -312,9 +339,24 @@ WITH RECURSIVE chain AS ( let run_by_id (module Conn : Caqti_async.CONNECTION) id = Conn.find_opt query_by_id id + let run_is_old_height (module Conn : Caqti_async.CONNECTION) ~height = + let open Deferred.Result.Let_syntax in + let open Int64 in + let%map max_height = + Conn.find query_max_height () + in + (* buffer an epsilon of 5 just in case archive node isn't caught up *) + let epsilon = of_int 5 in + height < (max_height - (of_int Genesis_constants.k)) - epsilon + let run (module Conn : Caqti_async.CONNECTION) = function | Some (`This (`Height h)) -> - Conn.find_opt query_height h + let open Deferred.Result.Let_syntax in + let%bind is_old_height = run_is_old_height (module Conn) ~height:h in + if is_old_height then + Conn.find_opt query_height_old h + else + Conn.find_opt query_height_recent h | Some (`That (`Hash h)) -> Conn.find_opt query_hash h | Some (`Those (`Height height, `Hash hash)) -> From 5c5b487477feb4e7954df0dabff530d93f58083d Mon Sep 17 00:00:00 2001 From: Brandon Kase Date: Wed, 10 Nov 2021 02:59:15 +0000 Subject: [PATCH 2/2] Adds comment about common_sql --- src/app/rosetta/lib/account.ml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/rosetta/lib/account.ml b/src/app/rosetta/lib/account.ml index dcc8c202f79..ae4f528d995 100644 --- a/src/app/rosetta/lib/account.ml +++ b/src/app/rosetta/lib/account.ml @@ -31,6 +31,9 @@ end module Sql = struct module Balance_from_last_relevant_command = struct + (* This is SQL is the common chunk shared between the query_recent and + * query_old that retreives the relevant balance changes from transactions + * at a block. *) let common_sql = {sql| relevant_internal_block_balances AS (