diff --git a/client-libs/kaitai-struct-files/files/alpha__operation.ksy b/client-libs/kaitai-struct-files/files/alpha__operation.ksy index 35d2fa1f99b7c74114ff27d51399d348243b8a1a..ebec4b7f2ecaad2ec6bce9d0a11656862b8a3278 100644 --- a/client-libs/kaitai-struct-files/files/alpha__operation.ksy +++ b/client-libs/kaitai-struct-files/files/alpha__operation.ksy @@ -136,18 +136,18 @@ types: type: bls_signature_prefix if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::signature_prefix) doc: The prefix of a BLS signature, i.e. the first 32 bytes. - - id: preattestation - type: preattestation - if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::preattestation) - id: attestation type: attestation if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestation) - - id: attestations_aggregate - type: attestations_aggregate - if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestations_aggregate) - id: attestation_with_dal type: attestation_with_dal if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestation_with_dal) + - id: preattestation + type: preattestation + if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::preattestation) + - id: attestations_aggregate + type: attestations_aggregate + if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestations_aggregate) - id: double_preattestation_evidence type: double_preattestation_evidence if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::double_preattestation_evidence) diff --git a/client-libs/kaitai-struct-files/files/alpha__operation__contents.ksy b/client-libs/kaitai-struct-files/files/alpha__operation__contents.ksy index 4cf6ecdfb976352c7d6d868894f9b80ec1fd7196..60a055a39546093ade892da99b11489d656e9c69 100644 --- a/client-libs/kaitai-struct-files/files/alpha__operation__contents.ksy +++ b/client-libs/kaitai-struct-files/files/alpha__operation__contents.ksy @@ -125,18 +125,18 @@ types: - id: alpha__operation__alpha__contents_tag type: u1 enum: alpha__operation__alpha__contents_tag - - id: preattestation - type: preattestation - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) - id: attestation type: attestation if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation) - - id: attestations_aggregate - type: attestations_aggregate - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: attestation_with_dal type: attestation_with_dal if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation_with_dal) + - id: preattestation + type: preattestation + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) + - id: attestations_aggregate + type: attestations_aggregate + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: double_preattestation_evidence type: double_preattestation_evidence if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::double_preattestation_evidence) diff --git a/client-libs/kaitai-struct-files/files/alpha__operation__contents_list.ksy b/client-libs/kaitai-struct-files/files/alpha__operation__contents_list.ksy index f024d3a35a89f17f588e35425e3db97315ba9b45..87051bc854f80a89b5dd79c6c19734629fd460ad 100644 --- a/client-libs/kaitai-struct-files/files/alpha__operation__contents_list.ksy +++ b/client-libs/kaitai-struct-files/files/alpha__operation__contents_list.ksy @@ -125,18 +125,18 @@ types: - id: alpha__operation__alpha__contents_tag type: u1 enum: alpha__operation__alpha__contents_tag - - id: preattestation - type: preattestation - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) - id: attestation type: attestation if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation) - - id: attestations_aggregate - type: attestations_aggregate - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: attestation_with_dal type: attestation_with_dal if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation_with_dal) + - id: preattestation + type: preattestation + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) + - id: attestations_aggregate + type: attestations_aggregate + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: double_preattestation_evidence type: double_preattestation_evidence if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::double_preattestation_evidence) diff --git a/client-libs/kaitai-struct-files/files/alpha__operation__protocol_data.ksy b/client-libs/kaitai-struct-files/files/alpha__operation__protocol_data.ksy index 668c27c3d91cd21d2e2256aa9210fd66e0d42f0b..35e2b790ce97387363b4f1802f7637061efddf01 100644 --- a/client-libs/kaitai-struct-files/files/alpha__operation__protocol_data.ksy +++ b/client-libs/kaitai-struct-files/files/alpha__operation__protocol_data.ksy @@ -136,18 +136,18 @@ types: type: bls_signature_prefix if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::signature_prefix) doc: The prefix of a BLS signature, i.e. the first 32 bytes. - - id: preattestation - type: preattestation - if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::preattestation) - id: attestation type: attestation if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestation) - - id: attestations_aggregate - type: attestations_aggregate - if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestations_aggregate) - id: attestation_with_dal type: attestation_with_dal if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestation_with_dal) + - id: preattestation + type: preattestation + if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::preattestation) + - id: attestations_aggregate + type: attestations_aggregate + if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::attestations_aggregate) - id: double_preattestation_evidence type: double_preattestation_evidence if: (alpha__operation__alpha__contents_or_signature_prefix_tag == alpha__operation__alpha__contents_or_signature_prefix_tag::double_preattestation_evidence) diff --git a/client-libs/kaitai-struct-files/files/alpha__operation__unsigned.ksy b/client-libs/kaitai-struct-files/files/alpha__operation__unsigned.ksy index 091ff1eeea1040d4ac35c44bd0a96a1ae1fd9f56..028c2cb1a62aee07b3dd28490e40a37510460318 100644 --- a/client-libs/kaitai-struct-files/files/alpha__operation__unsigned.ksy +++ b/client-libs/kaitai-struct-files/files/alpha__operation__unsigned.ksy @@ -125,18 +125,18 @@ types: - id: alpha__operation__alpha__contents_tag type: u1 enum: alpha__operation__alpha__contents_tag - - id: preattestation - type: preattestation - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) - id: attestation type: attestation if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation) - - id: attestations_aggregate - type: attestations_aggregate - if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: attestation_with_dal type: attestation_with_dal if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestation_with_dal) + - id: preattestation + type: preattestation + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::preattestation) + - id: attestations_aggregate + type: attestations_aggregate + if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::attestations_aggregate) - id: double_preattestation_evidence type: double_preattestation_evidence if: (alpha__operation__alpha__contents_tag == alpha__operation__alpha__contents_tag::double_preattestation_evidence) diff --git a/src/proto_alpha/lib_plugin/RPC.ml b/src/proto_alpha/lib_plugin/RPC.ml index bc975f8229d8bf60faa3bbfe05c2a7574a6a7f98..6449f4f7da32b7438ce85074843ac21010c870a3 100644 --- a/src/proto_alpha/lib_plugin/RPC.ml +++ b/src/proto_alpha/lib_plugin/RPC.ml @@ -3203,6 +3203,14 @@ module Forge = struct })) ~output:(obj1 (req "protocol_data" (bytes Hex))) RPC_path.(path / "protocol_data") + + let consensus_operations = + RPC_service.post_service + ~description:"Forge a consensus operation in BLS mode" + ~query:RPC_query.empty + ~input:Operation.unsigned_encoding + ~output:(obj2 (req "sign" (bytes Hex)) (req "inject" (bytes Hex))) + RPC_path.(path / "bls_consensus_operations") end let register () = @@ -3215,6 +3223,14 @@ module Forge = struct (Data_encoding.Binary.to_bytes_exn Operation.unsigned_encoding operation)) ; + Registration.register0_noctxt + ~chunked:true + S.consensus_operations + (fun () operation -> + return + Data_encoding.Binary. + ( to_bytes_exn Operation.bls_mode_unsigned_encoding operation, + to_bytes_exn Operation.unsigned_encoding operation )) ; Registration.register0_noctxt ~chunked:true S.protocol_data diff --git a/src/proto_alpha/lib_protocol/alpha_context.ml b/src/proto_alpha/lib_protocol/alpha_context.ml index a92901a9ab29f8179ef521d0184da214fa67a16c..40940a956160962462503cc0ce2792942f32bd64 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.ml +++ b/src/proto_alpha/lib_protocol/alpha_context.ml @@ -199,6 +199,8 @@ module Operation = struct let unsigned_encoding = unsigned_operation_encoding + let bls_mode_unsigned_encoding = bls_mode_unsigned_operation_encoding + include Operation_repr let check_signature _ctxt = check_signature unsigned_encoding diff --git a/src/proto_alpha/lib_protocol/alpha_context.mli b/src/proto_alpha/lib_protocol/alpha_context.mli index faf8b61f97abb7563642b42c5802b5f9321cdbcd..dc3aaf1657beb7324904d0f999844676dca6e692 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.mli +++ b/src/proto_alpha/lib_protocol/alpha_context.mli @@ -4913,6 +4913,12 @@ module Operation : sig val unsigned_encoding : (Operation.shell_header * packed_contents_list) Data_encoding.t + (* Encoding to sign and verify attestations signatures + with BLS keys. In this encoding, the signed payload is omitting slots to + enable BLS proof of possession aggregation. *) + val bls_mode_unsigned_encoding : + (Operation.shell_header * packed_contents_list) Data_encoding.t + type raw = Operation.t = {shell : Operation.shell_header; proto : bytes} val raw_encoding : raw Data_encoding.t @@ -4946,6 +4952,8 @@ module Operation : sig val unsigned_operation_length : _ operation -> int + val bls_mode_unsigned_operation_length : _ operation -> int + val check_signature : context -> public_key -> Chain_id.t -> _ operation -> unit tzresult diff --git a/src/proto_alpha/lib_protocol/operation_repr.ml b/src/proto_alpha/lib_protocol/operation_repr.ml index 5cbba36b9074f3942030776400154f7448d5e638..264f27be6bdf392a9033837721299af7a95026cd 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.ml +++ b/src/proto_alpha/lib_protocol/operation_repr.ml @@ -1528,12 +1528,10 @@ module Encoding = struct type packed_case = PCase : 'b case -> packed_case - let contents_cases = + let contents_cases_common = [ PCase preattestation_case; - PCase attestation_case; PCase attestations_aggregate_case; - PCase attestation_with_dal_case; PCase double_preattestation_evidence_case; PCase double_attestation_evidence_case; PCase seed_nonce_revelation_case; @@ -1568,6 +1566,10 @@ module Encoding = struct PCase zk_rollup_update_case; ] + let contents_cases = + PCase attestation_case :: PCase attestation_with_dal_case + :: contents_cases_common + let contents_encoding = let make (PCase (Case {tag; name; encoding; select; proj; inj})) = assert (not @@ reserved_tag tag) ; @@ -1751,6 +1753,71 @@ module Encoding = struct @@ merge_objs Operation.shell_header_encoding (obj1 (req "contents" contents_list_encoding)) + + (* Encoding to sign and verify attestations and preattestations signatures + with BLS keys. In this encoding, the signed payload is omitting slots to + enable BLS proof of possession aggregation. *) + module Bls_mode = struct + let attestation_case = + Case + { + tag = 41; + name = "bls_mode_attestation"; + encoding = + merge_objs + consensus_aggregate_content_encoding + (obj1 (opt "dal" dal_content_encoding)); + select = + (function Contents (Attestation _ as op) -> Some op | _ -> None); + proj = + (fun (Attestation {consensus_content; dal_content}) -> + ( { + level = consensus_content.level; + round = consensus_content.round; + block_payload_hash = consensus_content.block_payload_hash; + }, + Option.map + (fun dal_content -> dal_content.attestation) + dal_content )); + inj = + (fun ({level; round; block_payload_hash}, dal_content) -> + Attestation + { + consensus_content = + {slot = Slot_repr.zero; level; round; block_payload_hash}; + dal_content = + Option.map (fun attestation -> {attestation}) dal_content; + }); + } + + let contents_cases = + (* attestations without slots *) + PCase attestation_case :: contents_cases_common + + let contents_encoding = + let make (PCase (Case {tag; name; encoding; select; proj; inj})) = + assert (not @@ reserved_tag tag) ; + case + (Tag tag) + name + encoding + (fun o -> + match select o with None -> None | Some o -> Some (proj o)) + (fun x -> Contents (inj x)) + in + def "operation.alpha.bls_mode_contents" + @@ union (List.map make contents_cases) + + let contents_list_encoding = + conv_with_guard to_list of_list_internal (Variable.list contents_encoding) + + let encoding = + let open Data_encoding in + def "operation.alpha.bls_mode_unsigned_operation" + @@ merge_objs + Operation.shell_header_encoding + (obj1 (req "contents" contents_list_encoding)) + end end let encoding = Encoding.operation_encoding @@ -1763,6 +1830,8 @@ let protocol_data_encoding = Encoding.protocol_data_encoding let unsigned_operation_encoding = Encoding.unsigned_operation_encoding +let bls_mode_unsigned_operation_encoding = Encoding.Bls_mode.encoding + let raw ({shell; protocol_data} : _ operation) = let proto = Data_encoding.Binary.to_bytes_exn @@ -1868,6 +1937,12 @@ let unsigned_operation_length (type kind) unsigned_operation_encoding (shell, Contents_list protocol_data.contents) +let bls_mode_unsigned_operation_length (type kind) + ({shell; protocol_data} : kind operation) : int = + Data_encoding.Binary.length + bls_mode_unsigned_operation_encoding + (shell, Contents_list protocol_data.contents) + let check_signature (type kind) encoding key chain_id (op : kind operation) = let open Result_syntax in let serialized_operation = serialize_unsigned_operation encoding op in diff --git a/src/proto_alpha/lib_protocol/operation_repr.mli b/src/proto_alpha/lib_protocol/operation_repr.mli index f3847c6e0e1e4e711bffa30a0d988bc8d1572740..d730b31cd6932c014b18dd1ec5bbccd38b830a01 100644 --- a/src/proto_alpha/lib_protocol/operation_repr.mli +++ b/src/proto_alpha/lib_protocol/operation_repr.mli @@ -572,6 +572,12 @@ val protocol_data_encoding : packed_protocol_data Data_encoding.t val unsigned_operation_encoding : (Operation.shell_header * packed_contents_list) Data_encoding.t +(* Encoding to sign and verify attestations signatures with BLS keys. In this + encoding, the signed payload is omitting slots to enable BLS proof of + possession aggregation. *) +val bls_mode_unsigned_operation_encoding : + (Operation.shell_header * packed_contents_list) Data_encoding.t + val raw : _ operation -> raw val hash_raw : raw -> Operation_hash.t @@ -682,6 +688,10 @@ type error += Invalid_signature (* `Permanent *) signature. *) val unsigned_operation_length : _ operation -> int +(** Measuring the length of an operation in bls_mode, ignoring its + signature. *) +val bls_mode_unsigned_operation_length : _ operation -> int + (** Check the signature of an operation. This function serializes the operation using the provided encoding before calling the [Signature.check] function with the appropriate watermark. *) diff --git a/src/proto_alpha/lib_protocol/test/integration/consensus/test_attestation.ml b/src/proto_alpha/lib_protocol/test/integration/consensus/test_attestation.ml index 8f14d817b40dc193cc3c96754f53a4227f228ec9..ecfdbe32969625ee59b5e2bdacb1828ef848db98 100644 --- a/src/proto_alpha/lib_protocol/test/integration/consensus/test_attestation.ml +++ b/src/proto_alpha/lib_protocol/test/integration/consensus/test_attestation.ml @@ -824,6 +824,147 @@ let test_dal_attestation_threshold () = in return_unit +(* The BLS mode encoding differs from the regular attestation encoding + in that slots are omitted. This test verifies that an attestation's signature + check remains valid even after replacing its slot with a different one. *) +let slot_substitution_do_not_affect_signature_check () = + let open Lwt_result_syntax in + let* genesis, _contracts = Context.init_n 5 ~aggregate_attestation:true () in + let* b = Block.bake genesis in + let* delegate, _slots = Context.get_attester (B b) in + let* signer = Account.find delegate in + let watermark = Operation.to_watermark (Attestation Chain_id.zero) in + let slot_swap_and_check_signature slot + ({shell = {branch}; protocol_data = {contents; _}} : + Kind.attestation operation) = + match contents with + | Single (Attestation {consensus_content; dal_content}) -> + let bytes = + Data_encoding.Binary.to_bytes_exn + Operation.bls_mode_unsigned_encoding + ({branch}, Contents_list contents) + in + let signature = Signature.sign ~watermark signer.sk bytes in + let contents = + Single + (Attestation + {consensus_content = {consensus_content with slot}; dal_content}) + in + let bytes_with_different_slot = + Data_encoding.Binary.to_bytes_exn + Operation.bls_mode_unsigned_encoding + ({branch}, Contents_list contents) + in + if + Signature.check + ~watermark:(Operation.to_watermark (Attestation Chain_id.zero)) + signer.pk + signature + bytes_with_different_slot + then return_unit + else Test.fail "Unexpected signature check failure" + in + let* attestation_without_dal = + Op.raw_attestation ~delegate ~slot:Slot.zero b + in + let* () = + slot_swap_and_check_signature + (Slot.Internal_for_tests.of_int_unsafe_only_use_for_tests 1) + attestation_without_dal + in + let* attestation_with_dal = + (* attestation with dal signed with slot zero *) + Op.raw_attestation + ~dal_content:{attestation = Dal.Attestation.empty} + ~delegate + ~slot:Slot.zero + b + in + let* () = + slot_swap_and_check_signature + (Slot.Internal_for_tests.of_int_unsafe_only_use_for_tests 1) + attestation_with_dal + in + return_unit + +(* The BLS mode encoding differs from the regular attestation encoding in that + slots are omitted. This test ensures that an attestation's signature cannot + be mismatched between signing and verification. *) +let encoding_incompatibility () = + let open Lwt_result_syntax in + let* genesis, _contracts = Context.init_n 5 ~aggregate_attestation:true () in + let* b = Block.bake genesis in + let* delegate, _slots = Context.get_attester (B b) in + let* signer = Account.find delegate in + let check_encodings_incompatibily + ({shell = {branch}; protocol_data = {contents; signature = _}} : + Kind.attestation operation) = + let bytes_without_slot = + Data_encoding.Binary.to_bytes_exn + Operation.bls_mode_unsigned_encoding + ({branch}, Contents_list contents) + in + let bytes_with_slot = + Data_encoding.Binary.to_bytes_exn + Operation.unsigned_encoding + ({branch}, Contents_list contents) + in + let watermark = Operation.to_watermark (Attestation Chain_id.zero) in + let signature_without_slot = + Signature.sign ~watermark signer.sk bytes_without_slot + in + let signature_with_slot = + Signature.sign ~watermark signer.sk bytes_with_slot + in + (* Sanity checks *) + let* () = + if + Signature.check + ~watermark + signer.pk + signature_without_slot + bytes_without_slot + then return_unit + else + Test.fail "Unexpected signature check failure (signature_without_slot)" + in + let* () = + if + Signature.check ~watermark signer.pk signature_with_slot bytes_with_slot + then return_unit + else Test.fail "Unexpected signature check failure (signature_with_slot)" + in + (* Encodings incompatibility checks *) + let* () = + if + Signature.check + ~watermark + signer.pk + signature_without_slot + bytes_with_slot + then Test.fail "Unexpected signature check success (bytes_with_slot)" + else return_unit + in + if + Signature.check + ~watermark + signer.pk + signature_with_slot + bytes_without_slot + then Test.fail "Unexpected signature check success (bytes_without_slot)" + else return_unit + in + let* raw_attestation_without_dal = Op.raw_attestation ~delegate b in + let* () = check_encodings_incompatibily raw_attestation_without_dal in + let* raw_attestation_with_dal = + Op.raw_attestation + ~dal_content:{attestation = Dal.Attestation.empty} + ~delegate + b + in + let* () = check_encodings_incompatibily raw_attestation_with_dal in + return_unit + let tests = [ (* Positive tests *) @@ -887,6 +1028,13 @@ let tests = "DAL attestation_threshold" `Quick test_dal_attestation_threshold; + (* slots are not part of the signed payload *) + Tztest.tztest + "slot substitution do not affect signature check" + `Quick + slot_substitution_do_not_affect_signature_check; + (* bls_mode_unsigned_encoding cannot be mismatched with unsigned_encoding *) + Tztest.tztest "encoding incompatitiblity" `Quick encoding_incompatibility; ] let () =