From 8e0b2c91cfc0469511a274c7a49ec3a5169741f3 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 29 Apr 2022 13:03:22 +0100 Subject: [PATCH] Restore public `ccf::Receipt` type (#3793) --- .daily_canary | 2 +- CHANGELOG.md | 1 + CMakeLists.txt | 2 + cmake/common.cmake | 1 + doc/build_apps/api.rst | 2 +- doc/schemas/app_openapi.json | 85 +---- doc/schemas/gov_openapi.json | 80 +---- doc/schemas/node_openapi.json | 75 +--- include/ccf/claims_digest.h | 20 ++ include/ccf/crypto/sha256_hash.h | 6 +- include/ccf/historical_queries_interface.h | 17 +- include/ccf/receipt.h | 226 +++++++++--- samples/apps/logging/logging.cpp | 10 +- samples/apps/logging/logging_schema.h | 2 +- src/apps/js_generic/js_generic_base.cpp | 2 +- src/apps/js_v8/tmpl/receipt.cpp | 57 +-- src/apps/js_v8/tmpl/receipt.h | 4 +- src/crypto/sha256_hash.cpp | 18 +- src/endpoints/common_endpoint_registry.cpp | 4 +- src/js/historical.cpp | 135 +++---- src/js/wrap.cpp | 6 +- src/js/wrap.h | 2 +- src/node/historical_queries.h | 12 +- src/node/historical_queries_adapter.cpp | 128 +++++-- src/node/receipt.cpp | 359 +++++++++++++++++++ src/node/rpc/member_frontend.h | 2 +- src/node/rpc/node_frontend.h | 2 +- src/node/snapshot_serdes.h | 99 ++--- src/node/test/receipt.cpp | 195 ++++++++++ src/node/{tx_receipt.h => tx_receipt_impl.h} | 8 +- tests/infra/network.py | 2 +- tests/schema.py | 4 + 32 files changed, 1029 insertions(+), 539 deletions(-) create mode 100644 src/node/receipt.cpp create mode 100644 src/node/test/receipt.cpp rename src/node/{tx_receipt.h => tx_receipt_impl.h} (88%) diff --git a/.daily_canary b/.daily_canary index 73bf6089d9..882769db50 100644 --- a/.daily_canary +++ b/.daily_canary @@ -1 +1 @@ -Piou piou \ No newline at end of file +Cui cui cui cui \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a9136f4b17..7452569257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Primary node now also reports time at which the ack from each backup node was last received (`GET /node/consensus` endpoint). This can be used by operators to detect one-way partitions between the primary and backup nodes (#3769). +- Current receipt format is now exposed to C++ applications as `ccf::Receipt`, retrieved from `describe_receipt_v2`. Note that the previous JSON format is still available, but must be retrieved as a JSON object from `describe_receipt_v1`. ## [2.0.0-rc7] diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e5c6a6927..d3b28e3ee9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,6 +342,8 @@ if(BUILD_TESTS) add_unit_test( historical_queries_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/historical_queries.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/receipt.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/node/receipt.cpp ) target_link_libraries( historical_queries_test PRIVATE http_parser.host sss.host ccf_kv.host diff --git a/cmake/common.cmake b/cmake/common.cmake index b5c810e5c8..00fe5d3462 100644 --- a/cmake/common.cmake +++ b/cmake/common.cmake @@ -172,6 +172,7 @@ set(CCF_ENDPOINTS_SOURCES ${CCF_DIR}/src/indexing/strategies/seqnos_by_key_in_memory.cpp ${CCF_DIR}/src/indexing/strategies/visit_each_entry_in_map.cpp ${CCF_DIR}/src/node/historical_queries_adapter.cpp + ${CCF_DIR}/src/node/receipt.cpp ) find_library(CRYPTO_LIBRARY crypto) diff --git a/doc/build_apps/api.rst b/doc/build_apps/api.rst index 8c73a5fb0f..b29b6eb201 100644 --- a/doc/build_apps/api.rst +++ b/doc/build_apps/api.rst @@ -114,7 +114,7 @@ Historical Queries :project: CCF :members: -.. doxygenstruct:: ccf::TxReceipt +.. doxygenstruct:: ccf::Receipt :project: CCF :members: diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json index 610b24e9b9..262c87b942 100644 --- a/doc/schemas/app_openapi.json +++ b/doc/schemas/app_openapi.json @@ -158,7 +158,7 @@ "$ref": "#/components/schemas/string" }, "receipt": { - "$ref": "#/components/schemas/Receipt" + "$ref": "#/components/schemas/json" } }, "required": [ @@ -196,85 +196,6 @@ ], "type": "object" }, - "NodeId": { - "format": "hex", - "pattern": "^[a-f0-9]{64}$", - "type": "string" - }, - "Pem": { - "type": "string" - }, - "Pem_array": { - "items": { - "$ref": "#/components/schemas/Pem" - }, - "type": "array" - }, - "Receipt": { - "properties": { - "cert": { - "$ref": "#/components/schemas/string" - }, - "leaf": { - "$ref": "#/components/schemas/string" - }, - "leaf_components": { - "$ref": "#/components/schemas/Receipt__LeafComponents" - }, - "node_id": { - "$ref": "#/components/schemas/NodeId" - }, - "proof": { - "$ref": "#/components/schemas/Receipt__Element_array" - }, - "root": { - "$ref": "#/components/schemas/string" - }, - "service_endorsements": { - "$ref": "#/components/schemas/Pem_array" - }, - "signature": { - "$ref": "#/components/schemas/string" - } - }, - "required": [ - "signature", - "proof", - "node_id" - ], - "type": "object" - }, - "Receipt__Element": { - "properties": { - "left": { - "$ref": "#/components/schemas/string" - }, - "right": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, - "Receipt__Element_array": { - "items": { - "$ref": "#/components/schemas/Receipt__Element" - }, - "type": "array" - }, - "Receipt__LeafComponents": { - "properties": { - "claims_digest": { - "$ref": "#/components/schemas/string" - }, - "commit_evidence": { - "$ref": "#/components/schemas/string" - }, - "write_set_digest": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, "TransactionId": { "pattern": "^[0-9]+\\.[0-9]+$", "type": "string" @@ -322,7 +243,7 @@ "info": { "description": "This CCF sample app implements a simple logging application, securely recording messages at client-specified IDs. It demonstrates most of the features available to CCF apps.", "title": "CCF Sample Logging App", - "version": "1.9.2" + "version": "1.9.3" }, "openapi": "3.0.0", "paths": { @@ -1067,7 +988,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Receipt" + "$ref": "#/components/schemas/json" } } }, diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index f65ca0b9d1..9a96aa3f57 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -244,20 +244,9 @@ ], "type": "string" }, - "NodeId": { - "format": "hex", - "pattern": "^[a-f0-9]{64}$", - "type": "string" - }, "Pem": { "type": "string" }, - "Pem_array": { - "items": { - "$ref": "#/components/schemas/Pem" - }, - "type": "array" - }, "Proposal": { "properties": { "actions": { @@ -340,71 +329,6 @@ ], "type": "string" }, - "Receipt": { - "properties": { - "cert": { - "$ref": "#/components/schemas/string" - }, - "leaf": { - "$ref": "#/components/schemas/string" - }, - "leaf_components": { - "$ref": "#/components/schemas/Receipt__LeafComponents" - }, - "node_id": { - "$ref": "#/components/schemas/NodeId" - }, - "proof": { - "$ref": "#/components/schemas/Receipt__Element_array" - }, - "root": { - "$ref": "#/components/schemas/string" - }, - "service_endorsements": { - "$ref": "#/components/schemas/Pem_array" - }, - "signature": { - "$ref": "#/components/schemas/string" - } - }, - "required": [ - "signature", - "proof", - "node_id" - ], - "type": "object" - }, - "Receipt__Element": { - "properties": { - "left": { - "$ref": "#/components/schemas/string" - }, - "right": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, - "Receipt__Element_array": { - "items": { - "$ref": "#/components/schemas/Receipt__Element" - }, - "type": "array" - }, - "Receipt__LeafComponents": { - "properties": { - "claims_digest": { - "$ref": "#/components/schemas/string" - }, - "commit_evidence": { - "$ref": "#/components/schemas/string" - }, - "write_set_digest": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, "StateDigest": { "properties": { "state_digest": { @@ -481,7 +405,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "2.7.1" + "version": "2.7.2" }, "openapi": "3.0.0", "paths": { @@ -873,7 +797,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Receipt" + "$ref": "#/components/schemas/json" } } }, diff --git a/doc/schemas/node_openapi.json b/doc/schemas/node_openapi.json index cf7903905a..d7ef8ba9f4 100644 --- a/doc/schemas/node_openapi.json +++ b/doc/schemas/node_openapi.json @@ -519,12 +519,6 @@ "Pem": { "type": "string" }, - "Pem_array": { - "items": { - "$ref": "#/components/schemas/Pem" - }, - "type": "array" - }, "Quote": { "properties": { "endorsements": { @@ -563,71 +557,6 @@ }, "type": "array" }, - "Receipt": { - "properties": { - "cert": { - "$ref": "#/components/schemas/string" - }, - "leaf": { - "$ref": "#/components/schemas/string" - }, - "leaf_components": { - "$ref": "#/components/schemas/Receipt__LeafComponents" - }, - "node_id": { - "$ref": "#/components/schemas/NodeId" - }, - "proof": { - "$ref": "#/components/schemas/Receipt__Element_array" - }, - "root": { - "$ref": "#/components/schemas/string" - }, - "service_endorsements": { - "$ref": "#/components/schemas/Pem_array" - }, - "signature": { - "$ref": "#/components/schemas/string" - } - }, - "required": [ - "signature", - "proof", - "node_id" - ], - "type": "object" - }, - "Receipt__Element": { - "properties": { - "left": { - "$ref": "#/components/schemas/string" - }, - "right": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, - "Receipt__Element_array": { - "items": { - "$ref": "#/components/schemas/Receipt__Element" - }, - "type": "array" - }, - "Receipt__LeafComponents": { - "properties": { - "claims_digest": { - "$ref": "#/components/schemas/string" - }, - "commit_evidence": { - "$ref": "#/components/schemas/string" - }, - "write_set_digest": { - "$ref": "#/components/schemas/string" - } - }, - "type": "object" - }, "ReconfigurationType": { "enum": [ "OneTransaction", @@ -814,7 +743,7 @@ "info": { "description": "This API provides public, uncredentialed access to service and node state.", "title": "CCF Public Node API", - "version": "2.17.0" + "version": "2.17.1" }, "openapi": "3.0.0", "paths": { @@ -1164,7 +1093,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Receipt" + "$ref": "#/components/schemas/json" } } }, diff --git a/include/ccf/claims_digest.h b/include/ccf/claims_digest.h index b2edb823a3..9b55aa7b9a 100644 --- a/include/ccf/claims_digest.h +++ b/include/ccf/claims_digest.h @@ -45,4 +45,24 @@ namespace ccf return (is_set == other.is_set) && (digest == other.digest); } }; + + inline void to_json(nlohmann::json& j, const ClaimsDigest& hash) + { + j = hash.value(); + } + + inline void from_json(const nlohmann::json& j, ClaimsDigest& hash) + { + hash.set(j.get()); + } + + inline std::string schema_name(const ClaimsDigest*) + { + return ds::json::schema_name(); + } + + inline void fill_json_schema(nlohmann::json& schema, const ClaimsDigest*) + { + ds::json::fill_schema(schema); + } } \ No newline at end of file diff --git a/include/ccf/crypto/sha256_hash.h b/include/ccf/crypto/sha256_hash.h index bbf643c9e4..205d2c0506 100644 --- a/include/ccf/crypto/sha256_hash.h +++ b/include/ccf/crypto/sha256_hash.h @@ -40,7 +40,7 @@ namespace crypto std::string hex_str() const; static Sha256Hash from_hex_string(const std::string& str); - static Sha256Hash from_span(const std::span& sp); + static Sha256Hash from_span(const std::span& sp); static Sha256Hash from_representation(const Representation& r); }; @@ -48,6 +48,10 @@ namespace crypto void from_json(const nlohmann::json& j, Sha256Hash& hash); + std::string schema_name(const Sha256Hash*); + + void fill_json_schema(nlohmann::json& schema, const Sha256Hash*); + bool operator==(const Sha256Hash& lhs, const Sha256Hash& rhs); bool operator!=(const Sha256Hash& lhs, const Sha256Hash& rhs); diff --git a/include/ccf/historical_queries_interface.h b/include/ccf/historical_queries_interface.h index 4942192089..6a49222687 100644 --- a/include/ccf/historical_queries_interface.h +++ b/include/ccf/historical_queries_interface.h @@ -11,19 +11,6 @@ #include #include -namespace ccf -{ - // This is an opaque, incomplete type, but can be summarised to a - // JSON-serialisable form by the functions below - struct TxReceipt; - using TxReceiptPtr = std::shared_ptr; - - ccf::Receipt describe_receipt( - const TxReceipt& receipt, bool include_root = false); - ccf::Receipt describe_receipt( - const TxReceiptPtr& receipt_ptr, bool include_root = false); -} - namespace ccf::historical { struct State @@ -31,13 +18,13 @@ namespace ccf::historical /// Read-only historical store at transaction_id kv::ReadOnlyStorePtr store = nullptr; /// Receipt for ledger entry at transaction_id - TxReceiptPtr receipt = nullptr; + TxReceiptImplPtr receipt = nullptr; /// View and Sequence Number for the State ccf::TxID transaction_id; State( const kv::ReadOnlyStorePtr& store_, - const TxReceiptPtr& receipt_, + const TxReceiptImplPtr& receipt_, const ccf::TxID& transaction_id_) : store(store_), receipt(receipt_), diff --git a/include/ccf/receipt.h b/include/ccf/receipt.h index 5c6385afc5..a9259cce88 100644 --- a/include/ccf/receipt.h +++ b/include/ccf/receipt.h @@ -3,72 +3,196 @@ #pragma once +#include "ccf/claims_digest.h" #include "ccf/crypto/pem.h" +#include "ccf/crypto/sha256_hash.h" #include "ccf/ds/json.h" +#include "ccf/ds/openapi.h" #include "ccf/entity_id.h" #include +#include namespace ccf { - struct Receipt + class Receipt { - struct Element - { - std::optional left = std::nullopt; - std::optional right = std::nullopt; - }; + public: + virtual ~Receipt() = default; - struct LeafComponents - { - std::optional write_set_digest = std::nullopt; - std::optional commit_evidence = std::nullopt; - std::optional claims_digest = std::nullopt; + // Signature over the root digest, signed by the identity described in cert + std::vector signature = {}; + virtual crypto::Sha256Hash calculate_root() = 0; - LeafComponents() {} - LeafComponents( - const std::optional& write_set_digest_, - const std::optional& commit_evidence_, - const std::optional& claims_digest_) : - write_set_digest(write_set_digest_), - commit_evidence(commit_evidence_), - claims_digest(claims_digest_) - {} + ccf::NodeId node_id = {}; + crypto::Pem cert = {}; - bool operator==(const LeafComponents& other) const = default; - }; + std::vector service_endorsements = {}; - /// Signature over the root of the Merkle Tree, by the node private key - std::string signature; - /// Root of the Merkle Tree - std::optional root = std::nullopt; - /// Merkle proof from the signed root to the leaf components - std::vector proof = {}; - /// Node identity that produced the signature at the time - ccf::NodeId node_id; - /// Node identity as a PEM certificate - std::optional cert = std::nullopt; - // In practice, either leaf or leaf_components is set - /// Leaf of the Merkle proof, only set on transactions emitted by 1.x - /// networks. Corresponds to the write set digest. - std::optional leaf = std::nullopt; - /// Leaf components in transactions emitted by 2.x networks. - std::optional leaf_components = std::nullopt; - - std::optional> service_endorsements = std::nullopt; + virtual bool is_signature_transaction() const = 0; }; - DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Receipt::Element) - DECLARE_JSON_REQUIRED_FIELDS(Receipt::Element) - DECLARE_JSON_OPTIONAL_FIELDS(Receipt::Element, left, right) + // Most transactions produce a receipt constructed from a combination of 3 + // digests. Note that transactions emitted by old code versions may not + // include a claims_digest or a commit_evidence_digest, but from 2.0 onwards + // every transaction will contain a (potentially default-zero'd) claims digest + // and a commit evidence digest. + class ProofReceipt : public Receipt + { + public: + struct Components + { + crypto::Sha256Hash write_set_digest; + std::string commit_evidence; + ccf::ClaimsDigest claims_digest; + }; + Components leaf_components; - DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Receipt::LeafComponents) - DECLARE_JSON_REQUIRED_FIELDS(Receipt::LeafComponents) - DECLARE_JSON_OPTIONAL_FIELDS( - Receipt::LeafComponents, write_set_digest, commit_evidence, claims_digest) + struct ProofStep + { + enum + { + Left, + Right + } direction; - DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Receipt) - DECLARE_JSON_REQUIRED_FIELDS(Receipt, signature, proof, node_id) - DECLARE_JSON_OPTIONAL_FIELDS( - Receipt, root, cert, leaf, leaf_components, service_endorsements) + crypto::Sha256Hash hash = {}; + + bool operator==(const ProofStep& other) const + { + return direction == other.direction && hash == other.hash; + } + }; + using Proof = std::vector; + + // A merkle-tree path from the leaf digest to the signed root + Proof proof = {}; + + crypto::Sha256Hash calculate_root() override + { + auto current = get_leaf_digest(); + + for (const auto& element : proof) + { + if (element.direction == ProofStep::Left) + { + current = crypto::Sha256Hash(element.hash, current); + } + else + { + current = crypto::Sha256Hash(current, element.hash); + } + } + + return current; + } + + crypto::Sha256Hash get_leaf_digest() + { + crypto::Sha256Hash ce_dgst(leaf_components.commit_evidence); + if (!leaf_components.claims_digest.empty()) + { + return crypto::Sha256Hash( + leaf_components.write_set_digest, + ce_dgst, + leaf_components.claims_digest.value()); + } + else + { + return crypto::Sha256Hash(leaf_components.write_set_digest, ce_dgst); + } + } + + bool is_signature_transaction() const override + { + return false; + } + }; + + // Signature transactions are special, as they contain no proof. They contain + // a single root, which is directly signed. + class SignatureReceipt : public Receipt + { + public: + crypto::Sha256Hash signed_root = {}; + + crypto::Sha256Hash calculate_root() override + { + return signed_root; + }; + + bool is_signature_transaction() const override + { + return true; + } + }; + + using ReceiptPtr = std::shared_ptr; + + // This is an opaque, incomplete type, but can be summarised to a JSON object + // by describe_receipt_v1, or a ccf::ReceiptPtr by describe_receipt_v2 + struct TxReceiptImpl; + using TxReceiptImplPtr = std::shared_ptr; + nlohmann::json describe_receipt_v1(const TxReceiptImpl& receipt); + ReceiptPtr describe_receipt_v2(const TxReceiptImpl& receipt); + + // Manual JSON serializers are specified for these types as they are not + // trivial POD structs + + void to_json(nlohmann::json& j, const ProofReceipt::Components& step); + void from_json(const nlohmann::json& j, ProofReceipt::Components& step); + std::string schema_name(const ProofReceipt::Components*); + void fill_json_schema( + nlohmann::json& schema, const ProofReceipt::Components*); + + void to_json(nlohmann::json& j, const ProofReceipt::ProofStep& step); + void from_json(const nlohmann::json& j, ProofReceipt::ProofStep& step); + std::string schema_name(const ProofReceipt::ProofStep*); + void fill_json_schema(nlohmann::json& schema, const ProofReceipt::ProofStep*); + + void to_json(nlohmann::json& j, const ReceiptPtr& receipt); + void from_json(const nlohmann::json& j, ReceiptPtr& receipt); + std::string schema_name(const ReceiptPtr*); + void fill_json_schema(nlohmann::json& schema, const ReceiptPtr*); + + template + void add_schema_components( + T& helper, nlohmann::json& schema, const ProofReceipt::Components* comp) + { + helper.template add_schema_component(); + helper.template add_schema_component(); + + fill_json_schema(schema, comp); + } + + template + void add_schema_components( + T& helper, nlohmann::json& schema, const ProofReceipt::ProofStep* ps) + { + helper + .template add_schema_component(); + + fill_json_schema(schema, ps); + } + + template + void add_schema_components( + T& helper, nlohmann::json& schema, const ReceiptPtr* r) + { + helper.template add_schema_component(); + helper.template add_schema_component(); + helper + .template add_schema_component(); + helper.template add_schema_component(); + + helper.template add_schema_component(); + helper + .template add_schema_component(); + helper + .template add_schema_component(); + + fill_json_schema(schema, r); + } } diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index a5961c01df..d093e3d9fa 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -222,7 +222,7 @@ namespace loggingapp "This CCF sample app implements a simple logging application, securely " "recording messages at client-specified IDs. It demonstrates most of " "the features available to CCF apps."; - openapi_info.document_version = "1.9.2"; + openapi_info.document_version = "1.9.3"; index_per_public_key = std::make_shared( PUBLIC_RECORDS, context, 10000, 20); @@ -884,7 +884,7 @@ namespace loggingapp LoggingGetReceipt::Out out; out.msg = v.value(); assert(historical_state->receipt); - out.receipt = ccf::describe_receipt(historical_state->receipt); + out.receipt = ccf::describe_receipt_v1(*historical_state->receipt); ccf::jsonhandler::set_response(std::move(out), ctx.rpc_ctx, pack); } else @@ -938,10 +938,12 @@ namespace loggingapp out.msg = v.value(); assert(historical_state->receipt); // SNIPPET_START: claims_digest_in_receipt - out.receipt = ccf::describe_receipt(historical_state->receipt); // Claims are expanded as out.msg, so the claims digest is removed // from the receipt to force verification to re-compute it. - out.receipt.leaf_components->claims_digest = std::nullopt; + auto full_receipt = + ccf::describe_receipt_v1(*historical_state->receipt); + out.receipt = full_receipt; + out.receipt["leaf_components"].erase("claims_digest"); // SNIPPET_END: claims_digest_in_receipt ccf::jsonhandler::set_response(std::move(out), ctx.rpc_ctx, pack); } diff --git a/samples/apps/logging/logging_schema.h b/samples/apps/logging/logging_schema.h index 023c61504e..78734de9fa 100644 --- a/samples/apps/logging/logging_schema.h +++ b/samples/apps/logging/logging_schema.h @@ -42,7 +42,7 @@ namespace loggingapp struct Out { std::string msg; - ccf::Receipt receipt; + nlohmann::json receipt; }; }; diff --git a/src/apps/js_generic/js_generic_base.cpp b/src/apps/js_generic/js_generic_base.cpp index 65d0beb4ca..e1e3b807b7 100644 --- a/src/apps/js_generic/js_generic_base.cpp +++ b/src/apps/js_generic/js_generic_base.cpp @@ -270,7 +270,7 @@ namespace ccfapp ccf::endpoints::EndpointContext& endpoint_ctx, kv::ReadOnlyTx* historical_tx, const std::optional& transaction_id, - ccf::TxReceiptPtr receipt) + ccf::TxReceiptImplPtr receipt) { js::Runtime rt; rt.add_ccf_classdefs(); diff --git a/src/apps/js_v8/tmpl/receipt.cpp b/src/apps/js_v8/tmpl/receipt.cpp index 387574b33e..40526dbeb7 100644 --- a/src/apps/js_v8/tmpl/receipt.cpp +++ b/src/apps/js_v8/tmpl/receipt.cpp @@ -17,16 +17,18 @@ namespace ccf::v8_tmpl static ccf::Receipt* unwrap_receipt(v8::Local obj) { - return static_cast( + auto receipt_smart_ptr = static_cast( get_internal_field(obj, InternalField::Receipt)); + return receipt_smart_ptr->get(); } static void get_signature( v8::Local name, const v8::PropertyCallbackInfo& info) { ccf::Receipt* receipt = unwrap_receipt(info.Holder()); + const auto sig_b64 = crypto::b64_from_raw(receipt->signature); v8::Local value = - v8_util::to_v8_str(info.GetIsolate(), receipt->signature.c_str()); + v8_util::to_v8_str(info.GetIsolate(), sig_b64); info.GetReturnValue().Set(value); } @@ -34,24 +36,18 @@ namespace ccf::v8_tmpl v8::Local name, const v8::PropertyCallbackInfo& info) { ccf::Receipt* receipt = unwrap_receipt(info.Holder()); - v8::Local value; - if (receipt->cert.has_value()) - value = v8_util::to_v8_str(info.GetIsolate(), receipt->cert.value()); - else - value = v8::Undefined(info.GetIsolate()); + v8::Local value = + v8_util::to_v8_str(info.GetIsolate(), receipt->cert.str()); info.GetReturnValue().Set(value); } static void get_leaf( v8::Local name, const v8::PropertyCallbackInfo& info) { - ccf::Receipt* receipt = unwrap_receipt(info.Holder()); - v8::Local value; - if (receipt->leaf.has_value()) - value = v8_util::to_v8_str(info.GetIsolate(), receipt->leaf.value()); - else - value = v8::Undefined(info.GetIsolate()); - info.GetReturnValue().Set(value); + v8::Isolate* isolate = info.GetIsolate(); + v8::Local what = + v8_util::to_v8_str(isolate, "leaf is unimplemented in v8"); + isolate->ThrowException(what); } static void get_node_id( @@ -67,30 +63,9 @@ namespace ccf::v8_tmpl v8::Local name, const v8::PropertyCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); - v8::Local context = isolate->GetCurrentContext(); - ccf::Receipt* receipt = unwrap_receipt(info.Holder()); - - size_t size = receipt->proof.size(); - std::vector> elements; - elements.reserve(size); - for (auto& element : receipt->proof) - { - auto is_left = element.left.has_value(); - v8::Local obj = v8::Object::New(isolate); - obj - ->Set( - context, - v8_util::to_v8_istr(isolate, is_left ? "left" : "right"), - v8_util::to_v8_str( - isolate, (is_left ? element.left : element.right).value())) - .Check(); - elements.push_back(obj); - } - - v8::Local array = - v8::Array::New(info.GetIsolate(), elements.data(), size); - - info.GetReturnValue().Set(array); + v8::Local what = + v8_util::to_v8_str(isolate, "proof is unimplemented in v8"); + isolate->ThrowException(what); } v8::Local Receipt::create_template(v8::Isolate* isolate) @@ -114,12 +89,12 @@ namespace ccf::v8_tmpl } v8::Local Receipt::wrap( - v8::Local context, const ccf::TxReceipt& receipt) + v8::Local context, const ccf::TxReceiptImpl& receipt) { - ccf::Receipt* receipt_out = new ccf::Receipt(); + ccf::ReceiptPtr* receipt_out = new ccf::ReceiptPtr(); V8Context::from_context(context).register_finalizer( [](void* data) { delete static_cast(data); }, receipt_out); - *receipt_out = ccf::describe_receipt(receipt); + *receipt_out = ccf::describe_receipt_v2(receipt); v8::Isolate* isolate = context->GetIsolate(); v8::EscapableHandleScope handle_scope(isolate); diff --git a/src/apps/js_v8/tmpl/receipt.h b/src/apps/js_v8/tmpl/receipt.h index c4b6a4a559..672fd4af54 100644 --- a/src/apps/js_v8/tmpl/receipt.h +++ b/src/apps/js_v8/tmpl/receipt.h @@ -2,7 +2,7 @@ // Licensed under the Apache 2.0 License. #pragma once -#include "node/tx_receipt.h" +#include "node/tx_receipt_impl.h" #include @@ -15,7 +15,7 @@ namespace ccf::v8_tmpl static v8::Local create_template(v8::Isolate* isolate); static v8::Local wrap( - v8::Local context, const ccf::TxReceipt& receipt); + v8::Local context, const ccf::TxReceiptImpl& receipt); }; } // namespace ccf::v8_tmpl diff --git a/src/crypto/sha256_hash.cpp b/src/crypto/sha256_hash.cpp index 91dae10e54..10b8125523 100644 --- a/src/crypto/sha256_hash.cpp +++ b/src/crypto/sha256_hash.cpp @@ -70,7 +70,7 @@ namespace crypto return digest; } - Sha256Hash Sha256Hash::from_span(const std::span& sp) + Sha256Hash Sha256Hash::from_span(const std::span& sp) { Sha256Hash digest; std::copy(sp.begin(), sp.end(), digest.h.begin()); @@ -105,6 +105,22 @@ namespace crypto } } + std::string schema_name(const Sha256Hash*) + { + return "Sha256Digest"; + } + + void fill_json_schema(nlohmann::json& schema, const Sha256Hash*) + { + schema["type"] = "string"; + + // According to the spec, "format is an open value, so you can use any + // formats, even not those defined by the OpenAPI Specification" + // https://swagger.io/docs/specification/data-models/data-types/#format + schema["format"] = "hex"; + schema["pattern"] = fmt::format("^[a-f0-9]{{{}}}$", Sha256Hash::SIZE); + } + bool operator==(const Sha256Hash& lhs, const Sha256Hash& rhs) { for (unsigned i = 0; i < crypto::Sha256Hash::SIZE; i++) diff --git a/src/endpoints/common_endpoint_registry.cpp b/src/endpoints/common_endpoint_registry.cpp index a4c6335242..a2678b4880 100644 --- a/src/endpoints/common_endpoint_registry.cpp +++ b/src/endpoints/common_endpoint_registry.cpp @@ -236,7 +236,7 @@ namespace ccf ccf::jsonhandler::get_json_params(ctx.rpc_ctx); assert(historical_state->receipt); - ccf::Receipt out = ccf::describe_receipt(historical_state->receipt); + auto out = ccf::describe_receipt_v1(*historical_state->receipt); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); ccf::jsonhandler::set_response(out, ctx.rpc_ctx, pack); }; @@ -249,7 +249,7 @@ namespace ccf no_auth_required) .set_execute_outside_consensus( ccf::endpoints::ExecuteOutsideConsensus::Locally) - .set_auto_schema() + .set_auto_schema() .add_query_parameter(tx_id_param_key) .install(); } diff --git a/src/js/historical.cpp b/src/js/historical.cpp index 1705dc91f6..9c8c4c3572 100644 --- a/src/js/historical.cpp +++ b/src/js/historical.cpp @@ -7,84 +7,93 @@ namespace ccf::js #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc99-extensions" - static JSValue ccf_receipt_to_js(JSContext* ctx, TxReceiptPtr receipt) + static JSValue ccf_receipt_to_js(JSContext* ctx, TxReceiptImplPtr receipt) { - ccf::Receipt receipt_out = ccf::describe_receipt(receipt); + ccf::ReceiptPtr receipt_out_p = ccf::describe_receipt_v2(*receipt); + auto& receipt_out = *receipt_out_p; auto js_receipt = JS_NewObject(ctx); + + const auto sig_b64 = crypto::b64_from_raw(receipt_out.signature); + JS_SetPropertyStr( + ctx, js_receipt, "signature", JS_NewString(ctx, sig_b64.c_str())); + JS_SetPropertyStr( ctx, js_receipt, - "signature", - JS_NewString(ctx, receipt_out.signature.c_str())); - if (receipt_out.cert.has_value()) - JS_SetPropertyStr( - ctx, - js_receipt, - "cert", - JS_NewString(ctx, receipt_out.cert.value().c_str())); - if (receipt_out.leaf.has_value()) - { - JS_SetPropertyStr( - ctx, js_receipt, "leaf", JS_NewString(ctx, receipt_out.leaf->c_str())); - } - else if (receipt_out.leaf_components.has_value()) - { - auto leaf_components = JS_NewObject(ctx); - if (receipt_out.leaf_components->write_set_digest.has_value()) - { - JS_SetPropertyStr( - ctx, - leaf_components, - "write_set_digest", - JS_NewString( - ctx, receipt_out.leaf_components->write_set_digest->c_str())); - } + "cert", + JS_NewString(ctx, receipt_out.cert.str().c_str())); - if (receipt_out.leaf_components->commit_evidence.has_value()) - { - JS_SetPropertyStr( - ctx, - leaf_components, - "commit_evidence", - JS_NewString( - ctx, receipt_out.leaf_components->commit_evidence->c_str())); - } - - if (receipt_out.leaf_components->claims_digest.has_value()) - { - JS_SetPropertyStr( - ctx, - leaf_components, - "claims_digest", - JS_NewString( - ctx, receipt_out.leaf_components->claims_digest->c_str())); - } - JS_SetPropertyStr(ctx, js_receipt, "leaf_components", leaf_components); - } - else - { - throw std::logic_error("Receipt neither has leaf nor leaf_components"); - } JS_SetPropertyStr( ctx, js_receipt, "node_id", JS_NewString(ctx, receipt_out.node_id.value().c_str())); - auto proof = JS_NewArray(ctx); - uint32_t i = 0; - for (auto& element : receipt_out.proof) + + JS_SetPropertyStr( + ctx, + js_receipt, + "is_signature_transaction", + JS_NewBool(ctx, receipt_out.is_signature_transaction())); + + if (!receipt_out_p->is_signature_transaction()) { - auto js_element = JS_NewObject(ctx); - auto is_left = element.left.has_value(); + auto p_receipt = + std::dynamic_pointer_cast(receipt_out_p); + auto leaf_components = JS_NewObject(ctx); + const auto wsd_hex = + ds::to_hex(p_receipt->leaf_components.write_set_digest.h); JS_SetPropertyStr( ctx, - js_element, - is_left ? "left" : "right", - JS_NewString( - ctx, (is_left ? element.left : element.right).value().c_str())); - JS_DefinePropertyValueUint32(ctx, proof, i++, js_element, JS_PROP_C_W_E); + leaf_components, + "write_set_digest", + JS_NewString(ctx, wsd_hex.c_str())); + + JS_SetPropertyStr( + ctx, + leaf_components, + "commit_evidence", + JS_NewString(ctx, p_receipt->leaf_components.commit_evidence.c_str())); + + if (!p_receipt->leaf_components.claims_digest.empty()) + { + const auto cd_hex = + ds::to_hex(p_receipt->leaf_components.claims_digest.value().h); + JS_SetPropertyStr( + ctx, + leaf_components, + "claims_digest", + JS_NewString(ctx, cd_hex.c_str())); + } + + JS_SetPropertyStr(ctx, js_receipt, "leaf_components", leaf_components); + + auto proof = JS_NewArray(ctx); + uint32_t i = 0; + for (auto& element : p_receipt->proof) + { + auto js_element = JS_NewObject(ctx); + auto is_left = element.direction == ccf::ProofReceipt::ProofStep::Left; + const auto hash_hex = ds::to_hex(element.hash.h); + JS_SetPropertyStr( + ctx, + js_element, + is_left ? "left" : "right", + JS_NewString(ctx, hash_hex.c_str())); + JS_DefinePropertyValueUint32( + ctx, proof, i++, js_element, JS_PROP_C_W_E); + } + JS_SetPropertyStr(ctx, js_receipt, "proof", proof); } - JS_SetPropertyStr(ctx, js_receipt, "proof", proof); + else + { + auto sig_receipt = + std::dynamic_pointer_cast(receipt_out_p); + const auto signed_root = sig_receipt->signed_root; + const auto root_hex = ds::to_hex(signed_root.h); + JS_SetPropertyStr( + ctx, js_receipt, "root_hex", JS_NewString(ctx, root_hex.c_str())); + } + return js_receipt; } diff --git a/src/js/wrap.cpp b/src/js/wrap.cpp index 2d21d817e2..910543df3c 100644 --- a/src/js/wrap.cpp +++ b/src/js/wrap.cpp @@ -1511,7 +1511,7 @@ namespace ccf::js ReadOnlyTxContext* historical_txctx, ccf::RpcContext* rpc_ctx, const std::optional& transaction_id, - ccf::TxReceiptPtr receipt, + ccf::TxReceiptImplPtr receipt, ccf::AbstractGovernanceEffects* gov_effects, ccf::AbstractHostProcesses* host_processes, ccf::NetworkState* network_state, @@ -1804,7 +1804,7 @@ namespace ccf::js ReadOnlyTxContext* historical_txctx, ccf::RpcContext* rpc_ctx, const std::optional& transaction_id, - ccf::TxReceiptPtr receipt, + ccf::TxReceiptImplPtr receipt, ccf::AbstractGovernanceEffects* gov_effects, ccf::AbstractHostProcesses* host_processes, ccf::NetworkState* network_state, @@ -1837,7 +1837,7 @@ namespace ccf::js ReadOnlyTxContext* historical_txctx, ccf::RpcContext* rpc_ctx, const std::optional& transaction_id, - ccf::TxReceiptPtr receipt, + ccf::TxReceiptImplPtr receipt, ccf::AbstractGovernanceEffects* gov_effects, ccf::AbstractHostProcesses* host_processes, ccf::NetworkState* network_state, diff --git a/src/js/wrap.h b/src/js/wrap.h index 696faada70..07bc3b1b7d 100644 --- a/src/js/wrap.h +++ b/src/js/wrap.h @@ -180,7 +180,7 @@ namespace ccf::js ReadOnlyTxContext* historical_txctx, ccf::RpcContext* rpc_ctx, const std::optional& transaction_id, - ccf::TxReceiptPtr receipt, + ccf::TxReceiptImplPtr receipt, ccf::AbstractGovernanceEffects* gov_effects, ccf::AbstractHostProcesses* host_processes, ccf::NetworkState* network_state, diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index 5f5a4dd5bc..c832ecebd7 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -10,7 +10,7 @@ #include "node/history.h" #include "node/ledger_secrets.h" #include "node/rpc/node_interface.h" -#include "node/tx_receipt.h" +#include "node/tx_receipt_impl.h" #include "service/tables/node_signature.h" #include @@ -132,7 +132,7 @@ namespace ccf::historical ccf::ClaimsDigest claims_digest = {}; kv::StorePtr store = nullptr; bool is_signature = false; - TxReceiptPtr receipt = nullptr; + TxReceiptImplPtr receipt = nullptr; ccf::TxID transaction_id; bool has_commit_evidence = false; @@ -332,7 +332,7 @@ namespace ccf::historical { auto proof = tree.get_proof(seqno); details->transaction_id = {sig->view, seqno}; - details->receipt = std::make_shared( + details->receipt = std::make_shared( sig->sig, proof.get_root(), proof.get_path(), @@ -419,7 +419,7 @@ namespace ccf::historical { auto proof = tree.get_proof(new_seqno); new_details->transaction_id = {sig->view, new_seqno}; - new_details->receipt = std::make_shared( + new_details->receipt = std::make_shared( sig->sig, proof.get_root(), proof.get_path(), @@ -466,7 +466,7 @@ namespace ccf::historical { auto proof = tree.get_proof(new_seqno); new_details->transaction_id = {sig->view, new_seqno}; - new_details->receipt = std::make_shared( + new_details->receipt = std::make_shared( sig->sig, proof.get_root(), proof.get_path(), @@ -665,7 +665,7 @@ namespace ccf::historical const auto sig = get_signature(details->store); assert(sig.has_value()); details->transaction_id = {sig->view, sig->seqno}; - details->receipt = std::make_shared( + details->receipt = std::make_shared( sig->sig, sig->root.h, nullptr, sig->node, sig->cert); } diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 04aefe0a6a..f707621f23 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -7,79 +7,151 @@ #include "ccf/service/tables/service.h" #include "kv/kv_types.h" #include "node/rpc/network_identity_subsystem.h" -#include "node/tx_receipt.h" +#include "node/tx_receipt_impl.h" namespace ccf { static std::map> service_endorsement_cache; - ccf::Receipt describe_receipt(const TxReceipt& receipt, bool include_root) + nlohmann::json describe_receipt_v1(const TxReceiptImpl& receipt) { - ccf::Receipt out; - out.signature = crypto::b64_from_raw(receipt.signature); - if (include_root) - { - out.root = receipt.root.to_string(); - } + // Legacy JSON format, retained for compatibility + nlohmann::json out = nlohmann::json::object(); + + out["signature"] = crypto::b64_from_raw(receipt.signature); + + auto proof = nlohmann::json::array(); if (receipt.path != nullptr) { for (const auto& node : *receipt.path) { - ccf::Receipt::Element n; + auto n = nlohmann::json::object(); if (node.direction == ccf::HistoryTree::Path::Direction::PATH_LEFT) { - n.left = node.hash.to_string(); + n["left"] = node.hash.to_string(); } else { - n.right = node.hash.to_string(); + n["right"] = node.hash.to_string(); } - out.proof.emplace_back(std::move(n)); + proof.emplace_back(std::move(n)); } } - out.node_id = receipt.node_id; + out["proof"] = proof; + + out["node_id"] = receipt.node_id; if (receipt.node_cert.has_value()) { - out.cert = receipt.node_cert->str(); + out["cert"] = receipt.node_cert->str(); } if (receipt.path == nullptr) { // Signature transaction - out.leaf = receipt.root.to_string(); + out["leaf"] = receipt.root.to_string(); } else if (!receipt.commit_evidence.has_value()) { - out.leaf = receipt.write_set_digest->hex_str(); + out["leaf"] = receipt.write_set_digest->hex_str(); } else { - std::optional write_set_digest_str = std::nullopt; + auto leaf_components = nlohmann::json::object(); if (receipt.write_set_digest.has_value()) - write_set_digest_str = receipt.write_set_digest->hex_str(); - std::optional claims_digest_str = std::nullopt; + { + leaf_components["write_set_digest"] = + receipt.write_set_digest->hex_str(); + } + + if (receipt.commit_evidence.has_value()) + { + leaf_components["commit_evidence"] = receipt.commit_evidence.value(); + } + if (!receipt.claims_digest.empty()) - claims_digest_str = receipt.claims_digest.value().hex_str(); - out.leaf_components = Receipt::LeafComponents{ - write_set_digest_str, receipt.commit_evidence, claims_digest_str}; + { + leaf_components["claims_digest"] = + receipt.claims_digest.value().hex_str(); + } + out["leaf_components"] = leaf_components; } - out.service_endorsements = receipt.service_endorsements; + if (receipt.service_endorsements.has_value()) + { + out["service_endorsements"] = receipt.service_endorsements; + } return out; } - ccf::Receipt describe_receipt( - const TxReceiptPtr& receipt_ptr, bool include_root) + ccf::ReceiptPtr describe_receipt_v2(const TxReceiptImpl& in) { - if (receipt_ptr == nullptr) + ccf::ReceiptPtr receipt = nullptr; + + if (in.path != nullptr && in.commit_evidence.has_value()) { - throw std::runtime_error("Cannot describe nullptr receipt"); + auto proof_receipt = std::make_shared(); + + proof_receipt->proof.reserve(in.path->size()); + for (const auto& node : *in.path) + { + const auto direction = + node.direction == ccf::HistoryTree::Path::Direction::PATH_LEFT ? + ccf::ProofReceipt::ProofStep::Left : + ccf::ProofReceipt::ProofStep::Right; + const auto hash = crypto::Sha256Hash::from_span( + {node.hash.bytes, sizeof(node.hash.bytes)}); + proof_receipt->proof.push_back({direction, hash}); + } + + if (in.write_set_digest.has_value()) + { + proof_receipt->leaf_components.write_set_digest = + in.write_set_digest.value(); + } + + if (in.commit_evidence.has_value()) + { + proof_receipt->leaf_components.commit_evidence = + in.commit_evidence.value(); + } + + if (!in.claims_digest.empty()) + { + proof_receipt->leaf_components.claims_digest = in.claims_digest; + } + + receipt = proof_receipt; + } + else + { + // Signature transaction + auto sig_receipt = std::make_shared(); + sig_receipt->signed_root = + crypto::Sha256Hash::from_span({in.root.bytes, sizeof(in.root.bytes)}); + + receipt = sig_receipt; } - return describe_receipt(*receipt_ptr, include_root); + auto& out = *receipt; + + out.signature = in.signature; + + out.node_id = in.node_id; + + if (in.node_cert.has_value()) + { + out.cert = in.node_cert.value(); + } + + if (in.service_endorsements.has_value()) + { + out.service_endorsements = in.service_endorsements.value(); + } + + return receipt; } } diff --git a/src/node/receipt.cpp b/src/node/receipt.cpp new file mode 100644 index 0000000000..316e9fd6bb --- /dev/null +++ b/src/node/receipt.cpp @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "ccf/receipt.h" + +#define FROM_JSON_TRY_PARSE(TYPE, FIELD) \ + try \ + { \ + out.FIELD = it->get(); \ + } \ + catch (JsonParseError & jpe) \ + { \ + jpe.pointer_elements.push_back(#FIELD); \ + throw; \ + } + +#define FROM_JSON_GET_REQUIRED_FIELD(TYPE, FIELD) \ + { \ + const auto it = j.find(#FIELD); \ + if (it == j.end()) \ + { \ + throw JsonParseError(fmt::format( \ + "Missing required field '" #FIELD "' in object:", j.dump())); \ + } \ + FROM_JSON_TRY_PARSE(TYPE, FIELD) \ + } + +#define FROM_JSON_GET_OPTIONAL_FIELD(TYPE, FIELD) \ + { \ + const auto it = j.find(#FIELD); \ + if (it != j.end()) \ + { \ + FROM_JSON_TRY_PARSE(TYPE, FIELD) \ + } \ + } + +namespace ccf +{ + void to_json(nlohmann::json& j, const ProofReceipt::Components& components) + { + j = nlohmann::json::object(); + + j["write_set_digest"] = components.write_set_digest; + j["commit_evidence"] = components.commit_evidence; + + if (!components.claims_digest.empty()) + { + j["claims_digest"] = components.claims_digest; + } + } + + void from_json(const nlohmann::json& j, ProofReceipt::Components& out) + { + if (!j.is_object()) + { + throw JsonParseError(fmt::format( + "Cannot parse Receipt LeafComponents: Expected object, got {}", + j.dump())); + } + + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt::Components, write_set_digest); + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt::Components, commit_evidence); + + // claims_digest is always _emitted_ by current code, but may be + // missing from old receipts. When parsing those from JSON, treat it as + // optional + FROM_JSON_GET_OPTIONAL_FIELD(ProofReceipt::Components, claims_digest); + } + + std::string schema_name(const ProofReceipt::Components*) + { + return "Receipt__LeafComponents"; + } + + void fill_json_schema(nlohmann::json& schema, const ProofReceipt::Components*) + { + schema = nlohmann::json::object(); + schema["type"] = "object"; + + auto required = nlohmann::json::array(); + auto properties = nlohmann::json::object(); + + { + required.push_back("claims_digest"); + properties["claims_digest"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("commit_evidence"); + properties["commit_evidence"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("write_set_digest"); + properties["write_set_digest"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + } + + schema["required"] = required; + schema["properties"] = properties; + } + + void to_json(nlohmann::json& j, const ProofReceipt::ProofStep& step) + { + j = nlohmann::json::object(); + const auto key = + step.direction == ProofReceipt::ProofStep::Left ? "left" : "right"; + j[key] = step.hash; + } + + void from_json(const nlohmann::json& j, ProofReceipt::ProofStep& step) + { + if (!j.is_object()) + { + throw JsonParseError(fmt::format( + "Cannot parse Receipt Step: Expected object, got {}", j.dump())); + } + + const auto l_it = j.find("left"); + const auto r_it = j.find("right"); + if ((l_it == j.end()) == (r_it == j.end())) + { + throw JsonParseError(fmt::format( + "Cannot parse Receipt Step: Expected either 'left' or 'right' field, " + "got {}", + j.dump())); + } + + if (l_it != j.end()) + { + step.direction = ProofReceipt::ProofStep::Left; + step.hash = l_it.value(); + } + else + { + step.direction = ProofReceipt::ProofStep::Right; + step.hash = r_it.value(); + } + } + + std::string schema_name(const ProofReceipt::ProofStep*) + { + return "Receipt__Element"; + } + + void fill_json_schema(nlohmann::json& schema, const ProofReceipt::ProofStep*) + { + schema = nlohmann::json::object(); + + auto possible_hash = [](const auto& name) { + auto schema = nlohmann::json::object(); + schema["required"] = nlohmann::json::array(); + schema["required"].push_back(name); + schema["properties"] = nlohmann::json::object(); + schema["properties"][name] = ds::openapi::components_ref_object( + ds::json::schema_name()); + return schema; + }; + + schema["type"] = "object"; + schema["oneOf"] = nlohmann::json::array(); + schema["oneOf"].push_back(possible_hash("left")); + schema["oneOf"].push_back(possible_hash("right")); + } + + void to_json(nlohmann::json& j, const ReceiptPtr& receipt) + { + if (receipt == nullptr) + { + throw JsonParseError( + fmt::format("Cannot serialise Receipt to JSON: Got nullptr")); + } + + j = nlohmann::json::object(); + + j["signature"] = receipt->signature; + j["node_id"] = receipt->node_id; + j["cert"] = receipt->cert; + j["service_endorsements"] = receipt->service_endorsements; + j["is_signature_transaction"] = receipt->is_signature_transaction(); + + if (receipt->is_signature_transaction()) + { + throw std::logic_error( + "Conversion of signature receipts to JSON is currently undefined"); + } + else + { + auto p_receipt = std::dynamic_pointer_cast(receipt); + if (p_receipt == nullptr) + { + throw std::logic_error("Unexpected receipt type"); + } + + j["leaf_components"] = p_receipt->leaf_components; + j["proof"] = p_receipt->proof; + } + } + + void from_json(const nlohmann::json& j, ReceiptPtr& receipt) + { + if (!j.is_object()) + { + throw JsonParseError( + fmt::format("Cannot parse Receipt: Expected object, got {}", j.dump())); + } + + const auto is_sig_it = j.find("is_signature_transaction"); + if (is_sig_it != j.end()) + { + const bool is_sig = is_sig_it->get(); + + if (!is_sig) + { + auto p_receipt = std::make_shared(); + + auto& out = *p_receipt; + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt, leaf_components); + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt, proof); + + receipt = p_receipt; + } + else + { + throw JsonParseError(fmt::format( + "Cannot parse Receipt: Expected 'leaf_components' and 'proof'" + "fields, got {}", + j.dump())); + } + } + else + { + // An old receipt format! Look for leaf field or leaf_components, and + // parse to new representation accordingly + const auto leaf_it = j.find("leaf"); + const auto has_leaf = leaf_it != j.end(); + + const auto leaf_components_it = j.find("leaf_components"); + const auto has_leaf_components = leaf_components_it != j.end(); + + if (has_leaf && !has_leaf_components) + { + auto sig_receipt = std::make_shared(); + + try + { + sig_receipt->signed_root = + leaf_it->get(); + } + catch (JsonParseError& jpe) + { + jpe.pointer_elements.push_back("leaf"); + throw; + } + + receipt = sig_receipt; + } + else if (!has_leaf && has_leaf_components) + { + auto p_receipt = std::make_shared(); + + auto& out = *p_receipt; + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt, leaf_components); + FROM_JSON_GET_REQUIRED_FIELD(ProofReceipt, proof); + + receipt = p_receipt; + } + else + { + throw JsonParseError(fmt::format( + "Cannot parse v1 Receipt: Expected either 'leaf' or " + "'leaf_components' " + "field, got {}", + j.dump())); + } + } + + auto& out = *receipt; + FROM_JSON_GET_REQUIRED_FIELD(Receipt, signature); + FROM_JSON_GET_REQUIRED_FIELD(Receipt, node_id); + FROM_JSON_GET_REQUIRED_FIELD(Receipt, cert); + + // service_endorsements is always _emitted_ by current code, but may be + // missing from old receipts. When parsing those from JSON, treat it as + // optional + FROM_JSON_GET_OPTIONAL_FIELD(Receipt, service_endorsements); + } + + std::string schema_name(const ReceiptPtr*) + { + return "Receipt"; + } + + void fill_json_schema(nlohmann::json& schema, const ReceiptPtr*) + { + schema = nlohmann::json::object(); + schema["type"] = "object"; + + auto required = nlohmann::json::array(); + auto properties = nlohmann::json::object(); + + { + required.push_back("cert"); + properties["cert"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("node_id"); + properties["node_id"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("service_endorsements"); + properties["service_endorsements"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("signature"); + properties["signature"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + required.push_back("proof"); + properties["proof"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + properties["leaf_components"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + properties["leaf"] = ds::openapi::components_ref_object( + ds::json::schema_name()); + + // This says the required properties are all the properties we currently + // have, AND one of either leaf OR leaf_components. It inserts the + // following element into the schema, constructing a composite required + // list: + // "allOf": + // [ + // {"required": ["cert", "signature"...]}, + // { + // "oneOf": [ + // {"required": ["leaf"]}, + // {"required": ["leaf_components", "proof"]} + // ] + // } + // ] + const auto oneOf = nlohmann::json::object( + {{"oneOf", + nlohmann::json::array( + {nlohmann::json::object( + {{"required", nlohmann::json::array({"leaf"})}}), + nlohmann::json::object( + {{"required", + nlohmann::json::array({"leaf_components", "proof"})}})})}}); + + schema["allOf"] = nlohmann::json::array( + {nlohmann::json::object({{"required", required}}), oneOf}); + } + + schema["properties"] = properties; + } +} \ No newline at end of file diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 31d9e11e56..2284b8c02e 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -493,7 +493,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "2.7.1"; + openapi_info.document_version = "2.7.2"; } static std::optional get_caller_member_id( diff --git a/src/node/rpc/node_frontend.h b/src/node/rpc/node_frontend.h index 1e72c66f66..41e26745c9 100644 --- a/src/node/rpc/node_frontend.h +++ b/src/node/rpc/node_frontend.h @@ -357,7 +357,7 @@ namespace ccf openapi_info.description = "This API provides public, uncredentialed access to service and node " "state."; - openapi_info.document_version = "2.17.0"; + openapi_info.document_version = "2.17.1"; } void init_handlers() override diff --git a/src/node/snapshot_serdes.h b/src/node/snapshot_serdes.h index 559f0b7410..cb64265e63 100644 --- a/src/node/snapshot_serdes.h +++ b/src/node/snapshot_serdes.h @@ -9,68 +9,12 @@ #include "kv/kv_types.h" #include "kv/serialised_entry_format.h" #include "node/history.h" -#include "node/tx_receipt.h" +#include "node/tx_receipt_impl.h" #include namespace ccf { - /* Receipts included in snapshots always contain leaf components, - including a claims digest and commit evidence, from 2.0.0-rc0 onwards. - This verification code deliberately does not support snapshots - produced by 2.0.0-dev* releases - */ - static crypto::Sha256Hash compute_root_from_snapshot_receipt( - const Receipt& receipt) - { - crypto::Sha256Hash current; - if (receipt.leaf_components.has_value()) - { - auto components = receipt.leaf_components.value(); - if ( - components.write_set_digest.has_value() && - components.commit_evidence.has_value() && - components.claims_digest.has_value()) - { - auto ws_dgst = crypto::Sha256Hash::from_hex_string( - components.write_set_digest.value()); - crypto::Sha256Hash ce_dgst(components.commit_evidence.value()); - auto cl_dgst = - crypto::Sha256Hash::from_hex_string(components.claims_digest.value()); - current = crypto::Sha256Hash(ws_dgst, ce_dgst, cl_dgst); - } - else - { - throw std::logic_error( - "Cannot compute leaf unless write_set_digest, commit_evidence and " - "claims_digest " - "are set"); - } - } - else - { - throw std::logic_error( - "Cannot compute root if leaf_components are not set"); - } - for (auto const& element : receipt.proof) - { - if (element.left.has_value()) - { - assert(!element.right.has_value()); - auto left = crypto::Sha256Hash::from_hex_string(element.left.value()); - current = crypto::Sha256Hash(left, current); - } - else - { - assert(element.right.has_value()); - auto right = crypto::Sha256Hash::from_hex_string(element.right.value()); - current = crypto::Sha256Hash(current, right); - } - } - - return current; - } - struct StartupSnapshotInfo { std::vector raw; @@ -135,20 +79,18 @@ namespace ccf auto receipt_size = size - store_snapshot_size; auto j = nlohmann::json::parse(receipt_data, receipt_data + receipt_size); - auto receipt = j.get(); - - if ( - !receipt.leaf_components.has_value() || - !receipt.leaf_components->claims_digest.has_value()) + auto receipt_p = j.get(); + auto receipt = std::dynamic_pointer_cast(receipt_p); + if (receipt == nullptr) { throw std::logic_error( - "Snapshot receipt is missing snapshot digest claim"); + fmt::format("Unexpected receipt type: missing expanded claims")); } auto snapshot_digest = crypto::Sha256Hash({snapshot.data(), store_snapshot_size}); - auto snapshot_digest_claim = crypto::Sha256Hash::from_hex_string( - receipt.leaf_components->claims_digest.value()); + auto snapshot_digest_claim = + receipt->leaf_components.claims_digest.value(); if (snapshot_digest != snapshot_digest_claim) { throw std::logic_error(fmt::format( @@ -157,17 +99,15 @@ namespace ccf snapshot_digest_claim)); } - auto root = compute_root_from_snapshot_receipt(receipt); - auto raw_sig = crypto::raw_from_b64(receipt.signature); + auto root = receipt->calculate_root(); + auto raw_sig = receipt->signature; - if (!receipt.cert.has_value()) - { - throw std::logic_error("Missing node certificate in snapshot receipt"); - } - - auto v = crypto::make_unique_verifier(receipt.cert.value()); + auto v = crypto::make_unique_verifier(receipt->cert); if (!v->verify_hash( - root.h.data(), root.h.size(), raw_sig.data(), raw_sig.size())) + root.h.data(), + root.h.size(), + receipt->signature.data(), + receipt->signature.size())) { throw std::logic_error( "Signature verification failed for snapshot receipt"); @@ -176,7 +116,10 @@ namespace ccf if (prev_service_identity) { crypto::Pem prev_pem(*prev_service_identity); - if (!v->verify_certificate({&prev_pem}, {}, /* ignore_time */ true)) + if (!v->verify_certificate( + {&prev_pem}, + {}, /* ignore_time */ + true)) { throw std::logic_error( "Previous service identity does not endorse the node identity that " @@ -239,7 +182,7 @@ namespace ccf auto proof = history.get_proof(seqno); ccf::ClaimsDigest cd; cd.set(std::move(claims_digest)); - auto tx_receipt = std::make_shared( + ccf::TxReceiptImpl tx_receipt( sig, proof.get_root(), proof.get_path(), @@ -249,8 +192,8 @@ namespace ccf commit_evidence, cd); - Receipt receipt = ccf::describe_receipt(tx_receipt); - const auto receipt_str = nlohmann::json(receipt).dump(); + auto receipt = ccf::describe_receipt_v1(tx_receipt); + const auto receipt_str = receipt.dump(); return std::vector(receipt_str.begin(), receipt_str.end()); } } diff --git a/src/node/test/receipt.cpp b/src/node/test/receipt.cpp new file mode 100644 index 0000000000..95433270b6 --- /dev/null +++ b/src/node/test/receipt.cpp @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "ccf/receipt.h" + +#include "ccf/crypto/key_pair.h" +#include "ccf/service/tables/nodes.h" +#include "ds/x509_time_fmt.h" + +#include +#include +#include + +std::random_device rand_device; +std::default_random_engine rand_engine(rand_device()); + +crypto::Sha256Hash rand_digest() +{ + std::uniform_int_distribution dist; + + crypto::Sha256Hash ret; + std::generate( + ret.h.begin(), ret.h.end(), [&]() { return dist(rand_engine); }); + + return ret; +} + +void populate_receipt(std::shared_ptr receipt) +{ + using namespace std::literals; + const auto valid_from = + ds::to_x509_time_string(std::chrono::system_clock::now() - 1h); + const auto valid_to = + ds::to_x509_time_string(std::chrono::system_clock::now() + 1h); + + auto node_kp = crypto::make_key_pair(); + auto node_cert = node_kp->self_sign("CN=node", valid_from, valid_to); + + receipt->cert = node_cert; + receipt->node_id = ccf::compute_node_id_from_kp(node_kp); + + auto current_digest = receipt->get_leaf_digest(); + + const auto num_proof_steps = rand() % 8; + for (auto i = 0; i < num_proof_steps; ++i) + { + const auto dir = rand() % 2 == 0 ? ccf::ProofReceipt::ProofStep::Left : + ccf::ProofReceipt::ProofStep::Right; + const auto digest = rand_digest(); + + ccf::ProofReceipt::ProofStep step{dir, digest}; + receipt->proof.push_back(step); + + if (dir == ccf::ProofReceipt::ProofStep::Left) + { + current_digest = crypto::Sha256Hash(digest, current_digest); + } + else + { + current_digest = crypto::Sha256Hash(current_digest, digest); + } + } + + const auto root = receipt->calculate_root(); + receipt->signature = node_kp->sign_hash(root.h.data(), root.h.size()); + + const auto num_endorsements = rand() % 3; + for (auto i = 0; i < num_endorsements; ++i) + { + auto service_kp = crypto::make_key_pair(); + auto service_cert = + service_kp->self_sign("CN=service", valid_from, valid_to); + const auto csr = node_kp->create_csr(fmt::format("CN=Test{}", i)); + const auto endorsement = + service_kp->sign_csr(service_cert, csr, valid_from, valid_to); + receipt->service_endorsements.push_back(endorsement); + } +} + +void compare_receipts(ccf::ReceiptPtr l, ccf::ReceiptPtr r) +{ + REQUIRE(l != nullptr); + REQUIRE(r != nullptr); + + REQUIRE(l->signature == r->signature); + REQUIRE(l->node_id == r->node_id); + REQUIRE(l->cert == r->cert); + REQUIRE(l->service_endorsements == r->service_endorsements); + REQUIRE(l->is_signature_transaction() == r->is_signature_transaction()); + + if (!l->is_signature_transaction()) + { + auto p_l = std::dynamic_pointer_cast(l); + REQUIRE(p_l != nullptr); + + auto p_r = std::dynamic_pointer_cast(r); + REQUIRE(p_r != nullptr); + + REQUIRE(p_l->proof == p_r->proof); + REQUIRE( + p_l->leaf_components.write_set_digest == + p_r->leaf_components.write_set_digest); + REQUIRE( + p_l->leaf_components.commit_evidence == + p_r->leaf_components.commit_evidence); + REQUIRE( + p_l->leaf_components.claims_digest == p_r->leaf_components.claims_digest); + } + else + { + throw std::logic_error("Unhandled receipt type"); + } +} + +TEST_CASE("JSON parsing" * doctest::test_suite("receipt")) +{ + const auto sample_json_receipt = + R"xxx({ + "cert": "-----BEGIN CERTIFICATE-----\nMIIBzjCCAVSgAwIBAgIQGR/ue9CFspRa/g6jSMHFYjAKBggqhkjOPQQDAzAWMRQw\nEgYDVQQDDAtDQ0YgTmV0d29yazAeFw0yMjAxMjgxNjAzNDZaFw0yMjAxMjkxNjAz\nNDVaMBMxETAPBgNVBAMMCENDRiBOb2RlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE\nwsdpHLNw7xso/g71XzlQjoITiTBOef8gCayOiPJh/W2YfzreOawzD6gVQPSI+iPg\nZPc6smFhtV5bP/WZ2KW0K9Pn+OIjm/jMU5+s3rSgts50cRjlA/k81bUI88dzQzx9\no2owaDAJBgNVHRMEAjAAMB0GA1UdDgQWBBQgtPwYar54AQ4UL0RImVsm6wQQpzAf\nBgNVHSMEGDAWgBS2ngksRlVPvwDcLhN57VV+j2WyBTAbBgNVHREEFDAShwR/AAAB\nhwR/ZEUlhwR/AAACMAoGCCqGSM49BAMDA2gAMGUCMQDq54yS4Bmfwfcikpy2yL2+\nGFemyqNKXheFExRVt2edxVgId+uvIBGjrJEqf6zS/dsCMHVnBCLYRgxpamFkX1BF\nBDkVitfTOdYfUDWGV3MIMNdbam9BDNxG4q6XtQr4eb3jqg==\n-----END CERTIFICATE-----\n", + "leaf_components": { + "commit_evidence": "ce:2.643:55dbbbf04b71c6dcc01dd9d1c0012a6a959aef907398f7e183cc8913c82468d8", + "write_set_digest": "d0c521504ce2be6b4c22db8e99b14fc475b51bc91224181c75c64aa2cef72b83", + "claims_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "node_id": "7dfbb9a56ebe8b43c833b34cb227153ef61e4890187fe6164022255dec8f9646", + "proof": [ + { + "left": "00a771baf15468ed05d6ef8614b3669fcde6809314650061d64281b5d4faf9ec" + }, + { + "left": "a9c8a36d01aa9dfbfb74c6f6a2cef2efcbd92bd6dfd1f7440302ad5ac7be1577" + }, + { + "right": "8e238d95767e6ffe4b20e1a5e93dd7b926cbd86caa83698584a16ad2dd7d60b8" + }, + { + "left": "d4717996ae906cdce0ac47257a4a9445c58474c2f40811e575f804506e5fee9f" + }, + { + "left": "c1c206c4670bd2adee821013695d593f5983ca0994ae74630528da5fb6642205" + } + ], + "service_endorsements": [ + "-----BEGIN CERTIFICATE-----MIIBtTCCATugAwIBAgIRAN37fxGnWYNVLZn8nM8iBP8wCgYIKoZIzj0EAwMwFjEU\nMBIGA1UEAwwLQ0NGIE5ldHdvcmswHhcNMjIwMzIzMTMxMDA2WhcNMjIwMzI0MTMx\nMDA1WjAWMRQwEgYDVQQDDAtDQ0YgTmV0d29yazB2MBAGByqGSM49AgEGBSuBBAAi\nA2IABBErIfAEVg2Uw+iBPV9kEcpQw8NcoZWHmj4boHf7VVd6yCwRl+X/wOaOudca\nCqMMcwrt4Bb7n11RbsRwU04B7fG907MelICFHiPZjU/XMK5HEsSEZWowVtNwOLDo\nl5cN6aNNMEswCQYDVR0TBAIwADAdBgNVHQ4EFgQU4n5gHhHFnYZc3nwxKRggl8YB\nqdgwHwYDVR0jBBgwFoAUcAvR3F5YSUvPPGcAxrvh2Z5ump8wCgYIKoZIzj0EAwMD\naAAwZQIxAMeRoXo9FDzr51qkiD4Ws0Y+KZT06MFHcCg47TMDSGvnGrwL3DcIjGs7\nTTwJJQjbWAIwS9AqOJP24sN6jzXOTd6RokeF/MTGJbQAihzgTbZia7EKM8s/0yDB\n0QYtrfMjtPOx\n-----END CERTIFICATE-----\n" + ], + "signature": "MGQCMHrnwS123oHqUKuQRPsQ+gk6WVutixeOvxcXX79InBgPOxJCoScCOlBnK4UYyLzangIwW9k7IZkMgG076qVv5zcx7OuKb7bKyii1yP1rcakeGVvVMwISeE+Fr3BnFfPD66Df", + "is_signature_transaction": false +})xxx"; + + nlohmann::json j = nlohmann::json::parse(sample_json_receipt); + + auto receipt = j.get(); + + nlohmann::json j2 = receipt; + REQUIRE(j == j2); + + INFO("Check that old formats, with missing fields, can still be parsed"); + { + j.erase("service_endorsements"); + auto unendorsed = j.get(); + receipt->service_endorsements.clear(); + compare_receipts(receipt, unendorsed); + + j["leaf_components"].erase("claims_digest"); + REQUIRE_NOTHROW(j.get()); + } +} + +TEST_CASE("JSON roundtrip" * doctest::test_suite("receipt")) +{ + { + std::shared_ptr r = nullptr; + nlohmann::json j; + REQUIRE_THROWS(to_json(j, r)); + REQUIRE_THROWS(from_json(j, r)); + } + + for (auto i = 0; i < 20; ++i) + { + { + INFO("ProofReceipt"); + auto p_receipt = std::make_shared(); + p_receipt->leaf_components.write_set_digest = rand_digest(); + p_receipt->leaf_components.commit_evidence = "ce:2.4:abcd"; + p_receipt->leaf_components.claims_digest.set(rand_digest()); + + populate_receipt(p_receipt); + + nlohmann::json j = p_receipt; + + const auto parsed = j.get(); + compare_receipts(p_receipt, parsed); + } + } +} \ No newline at end of file diff --git a/src/node/tx_receipt.h b/src/node/tx_receipt_impl.h similarity index 88% rename from src/node/tx_receipt.h rename to src/node/tx_receipt_impl.h index 45998f3f52..97bdcdbc1f 100644 --- a/src/node/tx_receipt.h +++ b/src/node/tx_receipt_impl.h @@ -7,7 +7,9 @@ namespace ccf { - struct TxReceipt + // Representation of receipt used by internal framework code. Mirrored in + // public interface by ccf::Receipt + struct TxReceiptImpl { std::vector signature = {}; HistoryTree::Hash root = {}; @@ -19,7 +21,7 @@ namespace ccf ccf::ClaimsDigest claims_digest = {}; std::optional> service_endorsements = std::nullopt; - TxReceipt( + TxReceiptImpl( const std::vector& signature_, const HistoryTree::Hash& root_, std::shared_ptr path_, @@ -44,5 +46,5 @@ namespace ccf {} }; - using TxReceiptPtr = std::shared_ptr; + using TxReceiptImplPtr = std::shared_ptr; } diff --git a/tests/infra/network.py b/tests/infra/network.py index 322a5f3dbf..f071340017 100644 --- a/tests/infra/network.py +++ b/tests/infra/network.py @@ -1385,6 +1385,6 @@ def network( raise finally: LOG.info("Stopping network") - net.stop_all_nodes(skip_verification=True) + net.stop_all_nodes(skip_verification=True, accept_ledger_diff=True) if init_partitioner: net.partitioner.cleanup() diff --git a/tests/schema.py b/tests/schema.py index f7f39fd094..b84ccd56e4 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -80,6 +80,10 @@ def run(args): LOG.error(f"Writing to {alt_file} for comparison") with open(alt_file, "w", encoding="utf-8") as f2: f2.write(formatted_schema) + try: + old_schema.remove(alt_file) + except KeyError: + pass changed_files.append(openapi_target_file) else: LOG.debug("Schema matches in {}".format(openapi_target_file))