From 2ebd478a987f351733b71ab31ffab4137024cec5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:47:45 +0000 Subject: [PATCH 01/10] Initial plan From 1835e9f9642d485953ac3b5c802a6221a6a72776 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:51:13 +0000 Subject: [PATCH 02/10] Add describe_cose_receipt function and update logging app to use it Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ include/ccf/receipt.h | 4 ++++ samples/apps/logging/logging.cpp | 28 +++++-------------------- src/node/historical_queries_adapter.cpp | 23 ++++++++++++++++++++ 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83af4958d81..3df5c68e2558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [7.0.0-dev12]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.0-dev12 +### Added + +- Added `ccf::describe_cose_receipt(receipt)` to produce a complete COSE receipt from a `TxReceiptImpl`, combining signature and merkle proof. + ### Changed - Refactored the user facing surface of self-healing-open and local sealing. The whole feature is now `sealing-recovery` with `self-healing-open` now referred to as the `recovery-decision-protocol`. (#7679) diff --git a/include/ccf/receipt.h b/include/ccf/receipt.h index 267301e087b4..eac0fd6e1121 100644 --- a/include/ccf/receipt.h +++ b/include/ccf/receipt.h @@ -161,6 +161,10 @@ namespace ccf std::optional describe_cose_signature_v1( const TxReceiptImpl& receipt); + using SerialisedCoseReceipt = std::vector; + std::optional describe_cose_receipt( + const TxReceiptImpl& receipt); + // Manual JSON serializers are specified for these types as they are not // trivial POD structs diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index 00005bdf417e..d774880526c8 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -2143,41 +2143,23 @@ namespace loggingapp auto get_cose_receipt = []( ccf::endpoints::ReadOnlyEndpointContext& ctx, ccf::historical::StatePtr historical_state) { - auto historical_tx = historical_state->store->create_read_only_tx(); - assert(historical_state->receipt); - auto signature = describe_cose_signature_v1(*historical_state->receipt); - if (!signature.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - "No COSE signature available for this transaction"); - return; - } - auto proof = describe_merkle_proof_v1(*historical_state->receipt); - if (!proof.has_value()) + auto cose_receipt = + describe_cose_receipt(*historical_state->receipt); + if (!cose_receipt.has_value()) { ctx.rpc_ctx->set_error( HTTP_STATUS_NOT_FOUND, ccf::errors::ResourceNotFound, - "No merkle proof available for this transaction"); + "No COSE receipt available for this transaction"); return; } - constexpr int64_t vdp = 396; - auto inclusion_proof = ccf::cose::edit::pos::AtKey{-1}; - - ccf::cose::edit::desc::Value desc{inclusion_proof, vdp, *proof}; - - auto cose_receipt = - ccf::cose::edit::set_unprotected_header(*signature, desc); - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); ctx.rpc_ctx->set_response_header( ccf::http::headers::CONTENT_TYPE, ccf::http::headervalues::contenttype::COSE); - ctx.rpc_ctx->set_response_body(cose_receipt); + ctx.rpc_ctx->set_response_body(*cose_receipt); }; make_read_only_endpoint( "/log/public/cose_receipt", diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index a4dc4acc70dc..ea9953330e4f 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -3,6 +3,7 @@ #include "ccf/historical_queries_adapter.h" +#include "ccf/crypto/cose.h" #include "ccf/historical_queries_utils.h" #include "ccf/rpc_context.h" #include "ccf/service/tables/service.h" @@ -253,6 +254,28 @@ namespace ccf { return receipt.cose_signature; } + + std::optional describe_cose_receipt( + const TxReceiptImpl& receipt) + { + auto signature = describe_cose_signature_v1(receipt); + if (!signature.has_value()) + { + return std::nullopt; + } + + auto proof = describe_merkle_proof_v1(receipt); + if (!proof.has_value()) + { + return std::nullopt; + } + + constexpr int64_t vdp = 396; // inclusion-proofs label (draft-ietf-cose-merkle-tree-proofs) + auto inclusion_proof = ccf::cose::edit::pos::AtKey{-1}; + ccf::cose::edit::desc::Value desc{inclusion_proof, vdp, *proof}; + + return ccf::cose::edit::set_unprotected_header(*signature, desc); + } } namespace ccf::historical From 5e4d8c751f7b5e6a03f05eae1a01f7ba55acd2f5 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Mar 2026 05:55:30 -0800 Subject: [PATCH 03/10] Apply suggestion from @maxtropets --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df5c68e2558..4d90b038f11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added `ccf::describe_cose_receipt(receipt)` to produce a complete COSE receipt from a `TxReceiptImpl`, combining signature and merkle proof. +- Added `ccf::describe_cose_receipt(receipt)` to produce a complete COSE receipt from a `TxReceiptImpl` (#7700). ### Changed From 35d7e39a59eefa60fb9366ffe281d0ba8bf0fd18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:59:05 +0000 Subject: [PATCH 04/10] Address review: use named COSE constants, move SerialisedCoseReceipt type alias Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com> --- include/ccf/receipt.h | 3 +-- src/node/historical_queries_adapter.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/include/ccf/receipt.h b/include/ccf/receipt.h index eac0fd6e1121..2d50f00a5885 100644 --- a/include/ccf/receipt.h +++ b/include/ccf/receipt.h @@ -156,12 +156,11 @@ namespace ccf using SerialisedCoseEndorsement = std::vector; using SerialisedCoseSignature = std::vector; using SerialisedCoseEndorsements = std::vector; + using SerialisedCoseReceipt = std::vector; std::optional describe_cose_endorsements_v1( const TxReceiptImpl& receipt); std::optional describe_cose_signature_v1( const TxReceiptImpl& receipt); - - using SerialisedCoseReceipt = std::vector; std::optional describe_cose_receipt( const TxReceiptImpl& receipt); diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index ea9953330e4f..3a15aeb86031 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -8,6 +8,7 @@ #include "ccf/rpc_context.h" #include "ccf/service/tables/service.h" #include "crypto/cbor.h" +#include "crypto/cose.h" #include "kv/kv_types.h" #include "node/rpc/network_identity_subsystem.h" #include "node/tx_receipt_impl.h" @@ -270,9 +271,10 @@ namespace ccf return std::nullopt; } - constexpr int64_t vdp = 396; // inclusion-proofs label (draft-ietf-cose-merkle-tree-proofs) - auto inclusion_proof = ccf::cose::edit::pos::AtKey{-1}; - ccf::cose::edit::desc::Value desc{inclusion_proof, vdp, *proof}; + auto inclusion_proof = + ccf::cose::edit::pos::AtKey{ccf::cose::header::iana::INCLUSION_PROOFS}; + ccf::cose::edit::desc::Value desc{ + inclusion_proof, ccf::cose::header::iana::VDP, *proof}; return ccf::cose::edit::set_unprotected_header(*signature, desc); } From 4a29659c40914f440a537d7f340ce38add2cc9b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:22:15 +0000 Subject: [PATCH 05/10] Implement empty UHDR for signature TXs, Merkle proof in UHDR for non-signature TXs; update changelog Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com> --- CHANGELOG.md | 2 +- src/node/historical_queries_adapter.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d90b038f11a..4c84858c7b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added `ccf::describe_cose_receipt(receipt)` to produce a complete COSE receipt from a `TxReceiptImpl` (#7700). +- Added `ccf::describe_cose_receipt(receipt)` - includes Merkle proof in UHDR for non-signature TXs, empty UHDR for signature TXs (#7700). ### Changed diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 3a15aeb86031..75231b9bef2e 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -268,7 +268,8 @@ namespace ccf auto proof = describe_merkle_proof_v1(receipt); if (!proof.has_value()) { - return std::nullopt; + // Signature TX: return COSE signature as-is, with empty UHDR + return *signature; } auto inclusion_proof = From 7e1d771f5e2b3d4fc7c975620bbbb868a322c0f9 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Mar 2026 06:24:15 -0800 Subject: [PATCH 06/10] Apply suggestion from @maxtropets --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c84858c7b24..e1c3be2b1f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added `ccf::describe_cose_receipt(receipt)` - includes Merkle proof in UHDR for non-signature TXs, empty UHDR for signature TXs (#7700). +- Added `ccf::describe_cose_receipt(receipt)` to obtain COSE receipts with Merkle proof in unprotected header for non-signature TXs, and empty unprotected header for signature TXs (#7700). ### Changed From 029cf52e53ef633ffb437afdc6d5194eb18b52fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:33:16 +0000 Subject: [PATCH 07/10] Fix test_cbor_receipts to skip signature TXs with empty UHDR and iterate to find non-signature TX Co-authored-by: maxtropets <16566519+maxtropets@users.noreply.github.com> --- tests/e2e_logging.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 35c149c676e7..24aae29b34ab 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -910,10 +910,17 @@ def test_cbor_receipts(network, args): log_capture=[], # Do not emit raw binary to stdout ) if r.status_code == http.HTTPStatus.OK: - found_receipt = True cose_receipt = r.body.data() uhdr = cbor2.loads(cose_receipt).value[1] - proofs = uhdr[396][-1] + VDP_KEY = 396 # ccf::cose::header::iana::VDP + if VDP_KEY not in uhdr: + # Signature TX: valid receipt with empty UHDR, skip to next seqno + LOG.debug( + f"Transaction {txid} is a signature TX (empty UHDR), skipping" + ) + break + found_receipt = True + proofs = uhdr[VDP_KEY][-1] assert len(proofs) > 0, "No Merkle proofs found in receipt" r = client.get( From 5dd1e40f14e3af23ee3a3fb76f3ec738a5f139a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:38:31 +0000 Subject: [PATCH 08/10] Rename describe_cose_receipt to describe_cose_receipt_v1 for API consistency Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- CHANGELOG.md | 2 +- include/ccf/receipt.h | 2 +- samples/apps/logging/logging.cpp | 2 +- src/node/historical_queries_adapter.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef09924c28a..d43c58efc29e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added `ccf::describe_cose_receipt(receipt)` to obtain COSE receipts with Merkle proof in unprotected header for non-signature TXs, and empty unprotected header for signature TXs (#7700). +- Added `ccf::describe_cose_receipt_v1(receipt)` to obtain COSE receipts with Merkle proof in unprotected header for non-signature TXs, and empty unprotected header for signature TXs (#7700). - `NetworkIdentitySubsystemInterface` now exposes `get_trusted_keys()`, returning all trusted network identity keys as a `TrustedKeys` map (#7690). ### Changed diff --git a/include/ccf/receipt.h b/include/ccf/receipt.h index 2d50f00a5885..8ef4f601c11e 100644 --- a/include/ccf/receipt.h +++ b/include/ccf/receipt.h @@ -161,7 +161,7 @@ namespace ccf const TxReceiptImpl& receipt); std::optional describe_cose_signature_v1( const TxReceiptImpl& receipt); - std::optional describe_cose_receipt( + std::optional describe_cose_receipt_v1( const TxReceiptImpl& receipt); // Manual JSON serializers are specified for these types as they are not diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index 04854f88856e..f548ac090d3e 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -2180,7 +2180,7 @@ namespace loggingapp ccf::historical::StatePtr historical_state) { assert(historical_state->receipt); auto cose_receipt = - describe_cose_receipt(*historical_state->receipt); + describe_cose_receipt_v1(*historical_state->receipt); if (!cose_receipt.has_value()) { ctx.rpc_ctx->set_error( diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 75231b9bef2e..182c9760fea7 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -256,7 +256,7 @@ namespace ccf return receipt.cose_signature; } - std::optional describe_cose_receipt( + std::optional describe_cose_receipt_v1( const TxReceiptImpl& receipt) { auto signature = describe_cose_signature_v1(receipt); From 808fbe3303ff682414de73b25ae6705b9cb79b6a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 5 Mar 2026 03:46:09 -0800 Subject: [PATCH 09/10] Apply suggestion from @maxtropets --- tests/e2e_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 24aae29b34ab..d45b52efb766 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -912,7 +912,7 @@ def test_cbor_receipts(network, args): if r.status_code == http.HTTPStatus.OK: cose_receipt = r.body.data() uhdr = cbor2.loads(cose_receipt).value[1] - VDP_KEY = 396 # ccf::cose::header::iana::VDP + VDP_KEY = 396 if VDP_KEY not in uhdr: # Signature TX: valid receipt with empty UHDR, skip to next seqno LOG.debug( From a7b94697b601d6c37b420a441896bc8e76703010 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 5 Mar 2026 12:03:35 +0000 Subject: [PATCH 10/10] Apply suggestion from @achamayou --- src/node/historical_queries_adapter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 182c9760fea7..91be49da9420 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -269,7 +269,7 @@ namespace ccf if (!proof.has_value()) { // Signature TX: return COSE signature as-is, with empty UHDR - return *signature; + return signature; } auto inclusion_proof =