Skip to content

feat: add signer-based signing with sign_with, sign_with_operator and set_operator_with - #2682

Merged
danielmarv merged 9 commits into
hiero-ledger:mainfrom
danielmarv:feat/signer-based-signing
Oct 10, 2026
Merged

danielmarv merged 9 commits into
hiero-ledger:mainfrom
danielmarv:feat/signer-based-signing

Conversation

@danielmarv

Copy link
Copy Markdown
Member

Description:
This pull request adds the signer-based signing core described in #2678. Signing can now go through a callback, Signer = Callable[[bytes], bytes], instead of a PrivateKey held in the process. This lets keys stay in an HSM or KMS, and lets the client operator itself be signer-based. All existing signing paths (sign, execute, batchify and query payment) now route through it. This matches signWith, signWithOperator and setOperatorWith in the Java, JS and Go SDKs.

New API:

  • Signer type in crypto/signer.py, exported from the package root.
  • Transaction.sign_with(public_key, signer):
    • Requires a frozen transaction.
    • Calls the signer once per body, across all chunks and all nodes.
    • Is a no-op if the key is already registered or already present in every body's signature map, which covers signatures restored by from_bytes.
    • Collects every signature before writing any, so a signer that raises leaves the transaction unchanged.
    • Picks the ed25519 or ECDSA_secp256k1 field from the public key.
    • Raises TypeError if the signer is not callable or returns something other than bytes.
  • Transaction.sign_with_operator(client) raises ValueError without an operator, and freezes the transaction with the client first if it is not frozen.
  • Internal Transaction._apply_signers() re-signs bodies that lack a signature from a registered signer. It is the hook for rebuilt bodies (see feat(transaction): regenerate transaction ID on expiry #2657).
  • Client.set_operator_with(account_id, public_key, signer), with new operator_public_key and operator_signer properties.

Changes to existing code:

  • sign() is now a thin wrapper around sign_with; its behavior and deduplication are unchanged.
  • set_operator delegates to set_operator_with, still sets operator_private_key, and now returns self (previously None).
  • operator_public_key and operator_signer are derived from operator_private_key when it is set. Code that assigns client.operator_private_key directly therefore keeps signing.
  • Operator gains public_key and signer fields with defaults, so Operator(account_id, private_key) still works.
  • Query._build_query_payment_transaction signs with the operator's public key and signer, and reuses the shared signature-pair helper instead of its own copy. An Operator built by hand with only a private key still works.
  • execute() signs with the operator only when the operator is the payer, and no longer crashes with AttributeError on a client with no operator key.
  • batchify() calls sign_with_operator(client).

Behavior change:
A transaction whose payer is not the operator no longer gets the operator's signature automatically. If the operator is also a required signer, for example a token treasury or the sender in a transfer, callers must call sign_with_operator(client) explicitly. This matches the Java, JS and Go SDKs. The repo no longer has a CHANGELOG.md (removed in #2112), so the change is documented here.

Testing:

  • tests/unit/transaction_sign_with_test.py (27 tests) covers every case in the issue's test list. Highlights:
    • sign_with on multi-node and chunked FileAppendTransactions: one signer call per body, with the exact body bytes, and every signature verified.
    • Ed25519 and ECDSA signature fields.
    • Deduplication, the unfrozen error, signer failure leaving the transaction unchanged, and non-bytes signatures.
    • from_bytes deduplication and _apply_signers.
    • sign_with_operator, plus set_operator and set_operator_with compatibility.
    • The execute() payer rules, including a client with no operator.
    • Query payment and batchify() through a signer-based operator.
  • tests/integration/transaction_sign_with_e2e_test.py (Solo):
    • External signer with Ed25519 and ECDSA.
    • A set_operator_with client running a transaction and a paid query.
    • A non-operator payer, with no operator signature in the signature map.
    • A chunked FileAppendTransaction signed with sign_with.
  • Examples: examples/transaction/transaction_sign_with_external_signer.py and examples/client/client_set_operator_with.py.

Related issue(s):

Fixes #2678

Notes for reviewer:

  • All existing unit tests and TCK unit tests pass unchanged (3,244 in total, including the new ones).
  • The new integration tests and both examples pass against a local Solo network.
  • Full integration suite against Solo: 392 passed, 26 failed. Running the same 26 tests against main fails identically, so none are caused by this change:
    • Most are fee related, because the local operator 0.0.2 is a system account that Solo does not charge: insufficient-fee or insufficient-payment errors are not raised, transaction_fee is 0, and fee estimation fails.
    • The rest are mirror-node environment issues, mostly in mirror-node ID population and the topic message query (no mirror gRPC on localhost:5600).
  • Open questions from the issue that are not addressed here:

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

… set_operator_with

Route every signing path through a signer callback so keys can stay in an HSM or KMS.

- Add Signer type and Transaction.sign_with(public_key, signer); sign() now wraps it
- Add Transaction.sign_with_operator(client) and internal _apply_signers()
- Add Client.set_operator_with(account_id, public_key, signer); set_operator returns self
- Extend Operator with public_key and signer fields
- Sign query payments with the operator signer instead of the private key
- execute() signs with the operator only when the operator is the payer
- batchify() signs through sign_with_operator()

Signed-off-by: Ntege Daniel <danientege785@gmail.com>
@danielmarv
danielmarv requested review from a team as code owners October 1, 2026 10:56
@github-actions github-actions Bot added the approved Issue has been approved by team member label Oct 1, 2026
@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2682      +/-   ##
==========================================
+ Coverage   95.65%   95.69%   +0.04%     
==========================================
  Files         166      167       +1     
  Lines       10832    10890      +58     
==========================================
+ Hits        10361    10421      +60     
+ Misses        471      469       -2     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added open to community review PR is open for community review and feedback queue:junior-committer PR awaiting initial quality review labels Oct 1, 2026
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: hiero-ledger/hiero-sdk-python/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: e85385e4-744e-478f-b39b-44e1d359ab27


📥 Commits

Reviewing files that changed from the base of the PR and between 392e797 and 6ff0ee5.



📒 Files selected for processing (1)
  • examples/transaction/transaction_sign_with_external_signer.py


💤 Files with no reviewable changes (1)
  • examples/transaction/transaction_sign_with_external_signer.py


Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.




Walkthrough

The SDK adds callback-based signing for transactions and client operators. Transaction execution, batchification, and paid queries use signer callbacks. New examples and tests demonstrate these paths.

Changes

Signer-Based Signing

Layer / File(s) Summary
Signer API and operator credentials
src/hiero_sdk_python/crypto/signer.py, src/hiero_sdk_python/__init__.py, src/hiero_sdk_python/client/client.py, tests/unit/transaction_sign_with_test.py
The package exports Signer and provides a helper to build signature pairs from callback results. Client supports operators configured with a public key and signer, while retaining private-key setup. Unit tests cover operator configuration.
Transaction and query signing
src/hiero_sdk_python/transaction/transaction.py, src/hiero_sdk_python/query/query.py, tests/unit/transaction_sign_with_test.py
Transactions sign bodies through callbacks and retain registered signers for rebuilt bodies. Operator signing occurs when the operator is the payer. Paid-query payments use the operator public key and signer. Unit tests cover signature behavior, execution, batchification, and query payments.
Examples and integration coverage
examples/client/client_set_operator_with.py, examples/transaction/transaction_sign_with_external_signer.py, tests/integration/transaction_sign_with_e2e_test.py
The examples demonstrate callback-based operator configuration and external signing. Integration tests cover Ed25519 and ECDSA signatures, paid queries, payer selection, and chunked file appends.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Transaction
  participant SignerCallback
  participant SignatureMap
  Transaction->>SignerCallback: Send transaction body bytes
  SignerCallback-->>Transaction: Return signature bytes
  Transaction->>SignatureMap: Store signature pair
Loading

Merge Risk: 🔵 Low · up to 6ff0e

The example appears mergeable. Two existing edge cases remain: re-signing can miss a body added after signer registration, and an invalid manually configured query operator produces an unclear error. Neither was found on the example’s normal execution path.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 392e7

Signing authority remains application-configured, and automatic operator signing becomes more restrictive through a payer-match check. No concrete authorization bypass was established. External signer correctness, retained signing capabilities, and concurrent use still require clearly defined ownership and lifecycle guarantees.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A configured callback receives complete transaction or payment body bytes and may exercise the authority of its signing key. Effective account, asset, tenant, and environment scope depends on application and signer-provider policy; those restrictions are not defined by the callback type.

Security Findings and Attack Paths

  • inferred — No concrete introduced signing-authority escalation path was established in the inspected entrypoints. Callback registration is application-directed, automatic transaction signing is payer-gated, and additional operator signing requires an explicit caller action. This conclusion does not establish complete application or external-provider security coverage.

Trust Boundaries and Controls

  • observed — The application supplies the account/public-key/callback tuple. Registration does not verify their cryptographic correspondence. Signature construction checks that the callback returned bytes and selects the signature field from the supplied public key, but does not locally verify the signature. These are caller-contract dependencies, not evidence of an authorization bypass.

Resilience and Maintainability Implications

  • inferred — Collection-before-write protects local signature state but does not undo external signer calls already performed before failure. Deduplication and registration are unsynchronized check-then-act operations, so their sequential guarantees should not be extended to concurrent or reentrant use without an established ownership contract.

Hardening Proposals

  • proposed — Define the lifetime and release policy for registered signing capabilities, and use serialized transaction data rather than capability-bearing live objects when crossing into lower-trust consumers.
  • proposed — Specify single-owner, non-reentrant signing semantics or serialize the signing transition. Document that local failure containment does not roll back external signing operations.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the signer-based signing feature and names its main APIs.
Description check ✅ Passed The description explains the signer callback APIs, compatibility behavior, changes to signing paths, and tests.
Linked Issues check ✅ Passed The changes meet the coding requirements in [#2678]. Signer, Transaction.sign_with, sign_with_operator, and Client.set_operator_with are implemented. Existing signing paths use signer credenti…
Out of Scope Changes check ✅ Passed The whole-PR changes support [#2678] through signer implementation, tests, and examples. The latest example cleanup removes redundant load_dotenv setup. The unrelated TCK changes shown in the increm…
Docstring Coverage ✅ Passed Docstring coverage is 85.90% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 78 functions across 9 files.



✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR



  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@danielmarv danielmarv self-assigned this Oct 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: hiero-ledger/hiero-sdk-python/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 63fbe761-050c-4f0f-84b1-ca5932f38e0d

📥 Commits

Reviewing files that changed from the base of the PR and between 5d907b3 and 99728aa.

📒 Files selected for processing (4)
  • src/hiero_sdk_python/__init__.py
  • src/hiero_sdk_python/client/client.py
  • src/hiero_sdk_python/query/query.py
  • src/hiero_sdk_python/transaction/transaction.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/hiero_sdk_python/client/client.py
Comment thread src/hiero_sdk_python/query/query.py
Comment thread src/hiero_sdk_python/transaction/transaction.py
Comment thread src/hiero_sdk_python/transaction/transaction.py

@manishdait manishdait left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielmarv, great work!

Comment thread examples/transaction/transaction_sign_with_external_signer.py Outdated
Client.from_env() already loads .env.

Signed-off-by: Ntege Daniel <danientege785@gmail.com>

@manishdait manishdait left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@github-actions github-actions Bot added queue:committers PR awaiting committer technical review and removed queue:junior-committer PR awaiting initial quality review labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Hello, this is the OfficeHourBot.

This is a reminder that the Hiero Python SDK Office Hours will begin in approximately 3 hours and 55 minutes (14:00 UTC).

This session provides an opportunity to ask questions regarding this Pull Request.

Details:

Disclaimer: This is an automated reminder. Please verify the schedule here for any changes.

From,
The Python SDK Team

@aceppaluni aceppaluni added the status: update branch developer needs to click update branch label Oct 8, 2026
@aceppaluni

Copy link
Copy Markdown
Contributor

@danielmarv This looks great! Please update the branch when you have a moment. Thank you!!

@github-actions github-actions Bot added status: ready-to-merge PR has 1+ maintainer and 2+ total approvals, ready to merge and removed queue:committers PR awaiting committer technical review labels Oct 8, 2026
@danielmarv
danielmarv merged commit 97937d3 into hiero-ledger:main Oct 10, 2026
29 checks passed
@manishdait manishdait added this to the v0.3.0 milestone Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Issue has been approved by team member open to community review PR is open for community review and feedback status: ready-to-merge PR has 1+ maintainer and 2+ total approvals, ready to merge status: update branch developer needs to click update branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: signer-based signing core — sign_with, sign_with_operator, Client.set_operator_with

3 participants