(*****************************************************************************) (* *) (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) (* to deal in the Software without restriction, including without limitation *) (* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) (* and/or sell copies of the Software, and to permit persons to whom the *) (* Software is furnished to do so, subject to the following conditions: *) (* *) (* The above copyright notice and this permission notice shall be included *) (* in all copies or substantial portions of the Software. *) (* *) (* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) (* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) (* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) (* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) (* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) (* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) (* DEALINGS IN THE SOFTWARE. *) (* *) (*****************************************************************************) (** Block header *) type contents = { payload_hash : Block_payload_hash.t; payload_round : Round_repr.t; seed_nonce_hash : Nonce_hash.t option; proof_of_work_nonce : bytes; liquidity_baking_toggle_vote : Liquidity_baking_repr.liquidity_baking_toggle_vote; } type protocol_data = {contents : contents; signature : Signature.t} type t = {shell : Block_header.shell_header; protocol_data : protocol_data} type block_header = t type raw = Block_header.t type shell_header = Block_header.shell_header let raw_encoding = Block_header.encoding let shell_header_encoding = Block_header.shell_header_encoding type block_watermark = Block_header of Chain_id.t let bytes_of_block_watermark = function | Block_header chain_id -> Bytes.cat (Bytes.of_string "\x11") (Chain_id.to_bytes chain_id) let to_watermark b = Signature.Custom (bytes_of_block_watermark b) let of_watermark = function | Signature.Custom b -> if Compare.Int.(Bytes.length b > 0) then match Bytes.get b 0 with | '\x11' -> Option.map (fun chain_id -> Block_header chain_id) (Chain_id.of_bytes_opt (Bytes.sub b 1 (Bytes.length b - 1))) | _ -> None else None | _ -> None let contents_encoding = let open Data_encoding in def "block_header.alpha.unsigned_contents" @@ conv (fun { payload_hash; payload_round; seed_nonce_hash; proof_of_work_nonce; liquidity_baking_toggle_vote; } -> ( payload_hash, payload_round, proof_of_work_nonce, seed_nonce_hash, liquidity_baking_toggle_vote )) (fun ( payload_hash, payload_round, proof_of_work_nonce, seed_nonce_hash, liquidity_baking_toggle_vote ) -> { payload_hash; payload_round; seed_nonce_hash; proof_of_work_nonce; liquidity_baking_toggle_vote; }) (obj5 (req "payload_hash" Block_payload_hash.encoding) (req "payload_round" Round_repr.encoding) (req "proof_of_work_nonce" (Fixed.bytes Constants_repr.proof_of_work_nonce_size)) (opt "seed_nonce_hash" Nonce_hash.encoding) (req "liquidity_baking_toggle_vote" Liquidity_baking_repr.liquidity_baking_toggle_vote_encoding)) let protocol_data_encoding = let open Data_encoding in def "block_header.alpha.signed_contents" @@ conv (fun {contents; signature} -> (contents, signature)) (fun (contents, signature) -> {contents; signature}) (merge_objs contents_encoding (obj1 (req "signature" Signature.encoding))) let raw {shell; protocol_data} = let protocol_data = Data_encoding.Binary.to_bytes_exn protocol_data_encoding protocol_data in {Block_header.shell; protocol_data} let unsigned_encoding = let open Data_encoding in merge_objs Block_header.shell_header_encoding contents_encoding let encoding = let open Data_encoding in def "block_header.alpha.full_header" @@ conv (fun {shell; protocol_data} -> (shell, protocol_data)) (fun (shell, protocol_data) -> {shell; protocol_data}) (merge_objs Block_header.shell_header_encoding protocol_data_encoding) (** Constants *) let max_header_length = let fake_level = Raw_level_repr.root in let fake_round = Round_repr.zero in let fake_fitness = Fitness_repr.create_without_locked_round ~level:fake_level ~predecessor_round:fake_round ~round:fake_round in let fake_shell = { Block_header.level = 0l; proto_level = 0; predecessor = Block_hash.zero; timestamp = Time.of_seconds 0L; validation_passes = 0; operations_hash = Operation_list_list_hash.zero; fitness = Fitness_repr.to_raw fake_fitness; context = Context_hash.zero; } and fake_contents = { payload_hash = Block_payload_hash.zero; payload_round = Round_repr.zero; proof_of_work_nonce = Bytes.make Constants_repr.proof_of_work_nonce_size '0'; seed_nonce_hash = Some Nonce_hash.zero; liquidity_baking_toggle_vote = LB_pass; } in Data_encoding.Binary.length encoding { shell = fake_shell; protocol_data = {contents = fake_contents; signature = Signature.zero}; } (** Header parsing entry point *) let hash_raw = Block_header.hash let hash {shell; protocol_data} = Block_header.hash { shell; protocol_data = Data_encoding.Binary.to_bytes_exn protocol_data_encoding protocol_data; } type locked_round_evidence = { preendorsement_round : Round_repr.t; preendorsement_count : int; } type error += | (* Permanent *) Invalid_block_signature of Block_hash.t * Signature.Public_key_hash.t | (* Permanent *) Invalid_stamp | (* Permanent *) Invalid_payload_hash of { expected : Block_payload_hash.t; provided : Block_payload_hash.t; } | (* Permanent *) Locked_round_after_block_round of { locked_round : Round_repr.t; round : Round_repr.t; } | (* Permanent *) Invalid_payload_round of { payload_round : Round_repr.t; round : Round_repr.t; } | (* Permanent *) Insufficient_locked_round_evidence of { voting_power : int; consensus_threshold : int; } | (* Permanent *) Invalid_commitment of {expected : bool} | (* Permanent *) Wrong_timestamp of Time.t * Time.t let () = register_error_kind `Permanent ~id:"block_header.invalid_block_signature" ~title:"Invalid block signature" ~description:"A block was not signed with the expected private key." ~pp:(fun ppf (block, pkh) -> Format.fprintf ppf "Invalid signature for block %a. Expected: %a." Block_hash.pp_short block Signature.Public_key_hash.pp_short pkh) Data_encoding.( obj2 (req "block" Block_hash.encoding) (req "expected" Signature.Public_key_hash.encoding)) (function | Invalid_block_signature (block, pkh) -> Some (block, pkh) | _ -> None) (fun (block, pkh) -> Invalid_block_signature (block, pkh)) ; register_error_kind `Permanent ~id:"block_header.invalid_stamp" ~title:"Insufficient block proof-of-work stamp" ~description:"The block's proof-of-work stamp is insufficient" ~pp:(fun ppf () -> Format.fprintf ppf "Insufficient proof-of-work stamp") Data_encoding.empty (function Invalid_stamp -> Some () | _ -> None) (fun () -> Invalid_stamp) ; register_error_kind `Permanent ~id:"block_header.invalid_payload_hash" ~title:"Invalid payload hash" ~description:"Invalid payload hash." ~pp:(fun ppf (expected, provided) -> Format.fprintf ppf "Invalid payload hash (expected: %a, provided: %a)." Block_payload_hash.pp_short expected Block_payload_hash.pp_short provided) Data_encoding.( obj2 (req "expected" Block_payload_hash.encoding) (req "provided" Block_payload_hash.encoding)) (function | Invalid_payload_hash {expected; provided} -> Some (expected, provided) | _ -> None) (fun (expected, provided) -> Invalid_payload_hash {expected; provided}) ; () ; register_error_kind `Permanent ~id:"block_header.locked_round_after_block_round" ~title:"Locked round after block round" ~description:"Locked round after block round." ~pp:(fun ppf (locked_round, round) -> Format.fprintf ppf "Locked round (%a) is after the block round (%a)." Round_repr.pp locked_round Round_repr.pp round) Data_encoding.( obj2 (req "locked_round" Round_repr.encoding) (req "round" Round_repr.encoding)) (function | Locked_round_after_block_round {locked_round; round} -> Some (locked_round, round) | _ -> None) (fun (locked_round, round) -> Locked_round_after_block_round {locked_round; round}) ; () ; register_error_kind `Permanent ~id:"block_header.invalid_payload_round" ~title:"Invalid payload round" ~description:"The given payload round is invalid." ~pp:(fun ppf (payload_round, round) -> Format.fprintf ppf "The provided payload round (%a) is after the block round (%a)." Round_repr.pp payload_round Round_repr.pp round) Data_encoding.( obj2 (req "payload_round" Round_repr.encoding) (req "round" Round_repr.encoding)) (function | Invalid_payload_round {payload_round; round} -> Some (payload_round, round) | _ -> None) (fun (payload_round, round) -> Invalid_payload_round {payload_round; round}) ; register_error_kind `Permanent ~id:"block_header.insufficient_locked_round_evidence" ~title:"Insufficient locked round evidence" ~description:"Insufficient locked round evidence." ~pp:(fun ppf (voting_power, consensus_threshold) -> Format.fprintf ppf "The provided locked round evidence is not sufficient: provided %d \ voting power but was expecting at least %d." voting_power consensus_threshold) Data_encoding.( obj2 (req "voting_power" int31) (req "consensus_threshold" int31)) (function | Insufficient_locked_round_evidence {voting_power; consensus_threshold} -> Some (voting_power, consensus_threshold) | _ -> None) (fun (voting_power, consensus_threshold) -> Insufficient_locked_round_evidence {voting_power; consensus_threshold}) ; register_error_kind `Permanent ~id:"block_header.invalid_commitment" ~title:"Invalid commitment in block header" ~description:"The block header has invalid commitment." ~pp:(fun ppf expected -> if expected then Format.fprintf ppf "Missing seed's nonce commitment in block header." else Format.fprintf ppf "Unexpected seed's nonce commitment in block header.") Data_encoding.(obj1 (req "expected" bool)) (function Invalid_commitment {expected} -> Some expected | _ -> None) (fun expected -> Invalid_commitment {expected}) ; register_error_kind `Permanent ~id:"block_header.wrong_timestamp" ~title:"Wrong timestamp" ~description:"Block timestamp not the expected one." ~pp:(fun ppf (block_ts, expected_ts) -> Format.fprintf ppf "Wrong timestamp: block timestamp (%a) not the expected one (%a)" Time.pp_hum block_ts Time.pp_hum expected_ts) Data_encoding.( obj2 (req "block_timestamp" Time.encoding) (req "expected_timestamp" Time.encoding)) (function Wrong_timestamp (t1, t2) -> Some (t1, t2) | _ -> None) (fun (t1, t2) -> Wrong_timestamp (t1, t2)) let check_signature (block : t) (chain_id : Chain_id.t) (key : Signature.Public_key.t) = let check_signature key ({shell; protocol_data = {contents; signature}} : t) = let unsigned_header = Data_encoding.Binary.to_bytes_exn unsigned_encoding (shell, contents) in Signature.check ~watermark:(to_watermark (Block_header chain_id)) key signature unsigned_header in if check_signature key block then ok () else error (Invalid_block_signature (hash block, Signature.Public_key.hash key)) let check_payload_round ~round ~payload_round = error_when Round_repr.(payload_round > round) (Invalid_payload_round {payload_round; round}) let check_timestamp round_durations ~timestamp ~round ~predecessor_timestamp ~predecessor_round = Round_repr.timestamp_of_round round_durations ~predecessor_timestamp ~predecessor_round ~round >>? fun expected_timestamp -> if Time_repr.(expected_timestamp = timestamp) then Error_monad.ok () else error (Wrong_timestamp (timestamp, expected_timestamp)) module Proof_of_work = struct let check_hash hash stamp_threshold = let bytes = Block_hash.to_bytes hash in let word = TzEndian.get_int64 bytes 0 in Compare.Uint64.(word <= stamp_threshold) let check_header_proof_of_work_stamp shell contents stamp_threshold = let hash = hash {shell; protocol_data = {contents; signature = Signature.zero}} in check_hash hash stamp_threshold let check_proof_of_work_stamp ~proof_of_work_threshold block = if check_header_proof_of_work_stamp block.shell block.protocol_data.contents proof_of_work_threshold then ok () else error Invalid_stamp end let begin_validate_block_header ~(block_header : t) ~(chain_id : Chain_id.t) ~(predecessor_timestamp : Time.t) ~(predecessor_round : Round_repr.t) ~(fitness : Fitness_repr.t) ~(timestamp : Time.t) ~(delegate_pk : Signature.Public_key.t) ~(round_durations : Round_repr.Durations.t) ~(proof_of_work_threshold : int64) ~(expected_commitment : bool) = (* Level relationship between current node and the predecessor is done by the shell. We know that level is predecessor level + 1. The predecessor block hash is guaranteed by the shell to be the one in the shell header. The operations are guaranteed to correspond to the shell_header.operations_hash by the shell *) let {payload_round; seed_nonce_hash; _} = block_header.protocol_data.contents in let raw_level = block_header.shell.level in Proof_of_work.check_proof_of_work_stamp ~proof_of_work_threshold block_header >>? fun () -> Raw_level_repr.of_int32 raw_level >>? fun level -> check_signature block_header chain_id delegate_pk >>? fun () -> let round = Fitness_repr.round fitness in check_payload_round ~round ~payload_round >>? fun () -> check_timestamp round_durations ~predecessor_timestamp ~predecessor_round ~timestamp ~round >>? fun () -> Fitness_repr.check_except_locked_round fitness ~level ~predecessor_round >>? fun () -> let has_commitment = match seed_nonce_hash with None -> false | Some _ -> true in error_unless Compare.Bool.(has_commitment = expected_commitment) (Invalid_commitment {expected = expected_commitment}) type checkable_payload_hash = | No_check | Expected_payload_hash of Block_payload_hash.t let finalize_validate_block_header ~(block_header_contents : contents) ~(round : Round_repr.t) ~(* We have to check the round because in the construction case it was deduced from the time *) (fitness : Fitness_repr.t) ~(checkable_payload_hash : checkable_payload_hash) ~(locked_round_evidence : locked_round_evidence option) ~(consensus_threshold : int) = let { payload_hash = actual_payload_hash; seed_nonce_hash = _; proof_of_work_nonce = _; _; } = block_header_contents in (match checkable_payload_hash with | No_check -> Result.return_unit | Expected_payload_hash bph -> error_unless (Block_payload_hash.equal actual_payload_hash bph) (Invalid_payload_hash {expected = bph; provided = actual_payload_hash})) >>? fun () -> (match locked_round_evidence with | None -> ok None | Some {preendorsement_count; preendorsement_round} -> error_when Round_repr.(preendorsement_round >= round) (Locked_round_after_block_round {locked_round = preendorsement_round; round}) >>? fun () -> error_when Compare.Int.(preendorsement_count < consensus_threshold) (Insufficient_locked_round_evidence {voting_power = preendorsement_count; consensus_threshold}) >>? fun () -> ok (Some preendorsement_round)) >>? fun locked_round -> Fitness_repr.check_locked_round fitness ~locked_round