Skip to content

feat(external-call): minimal protocol integration#567

Merged
matthiasS-da merged 13 commits into
digital-asset:mainfrom
zenith-network:angelol/external-call-06b-protocol-integration
Jul 7, 2026
Merged

feat(external-call): minimal protocol integration#567
matthiasS-da merged 13 commits into
digital-asset:mainfrom
zenith-network:angelol/external-call-06b-protocol-integration

Conversation

@angelol

@angelol angelol commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR completes the minimal protocol integration of external calls (PR "6b" of the series tracked in #513, as agreed with @matthiasS-da): recorded external-call results are validated during transaction confirmation, and any disagreement rejects the request on behalf of all hosted confirming parties.

The external-call stack (#513): #506 added the transaction-side representation for recording external-call results; #514 added the LF EXTERNAL_CALL builtin surface; #518 wired it into Speedy and the LF engine; #522 added transaction protobuf encoding/decoding; #526 added Canton protocol serialization for recorded results; #537 added the participant-side extension-service client; #549#554 added hashing (V4), the prepared-transaction codec, errors and the handler SPI, engine execution and the validation SPI, extension-service wiring, and the consistency checker. This PR adds the missing runtime consumer.

Scope

  • ExternalCallCheck: a validation suite invoked from doParallelChecks in TransactionProcessingSteps, following the ModelConformanceChecker.check pattern (kicked off there, awaited when confirmation responses are created, network I/O concurrent with the other suites). It checks consistency of the recorded results across the request (ExternalCallConsistencyChecker; every visible disagreement is alarmed) and re-validates undisputed recorded outputs against the configured extension service, once per distinct call.
  • Verdicts (via the response factory's existing per-view verdict chain — its structure is unchanged): a disagreement from either part rejects each view on behalf of all its hosted confirming parties (LOCAL_VERDICT_EXTERNAL_CALL_RESULT_DISAGREEMENT); a recorded result that cannot be re-validated (e.g. no extension service configured) downgrades approvals to CANNOT_PERFORM_ALL_VALIDATIONS abstentions. One consequence of keeping the factory's first-match chain: the new non-malformed rejection ranks with the other consistency rejections, so — like the pre-existing time and contract-consistency rejections — it is reported in preference to a co-occurring malformed rejection from the later suites (authentication, model conformance, internal consistency).
  • The consistency checker now takes the participant views directly (view validation results do not exist yet at doParallelChecks time), and the ExternalCallValidator binding is threaded through the participant wiring.
  • Basic test coverage for the check outcomes and their response translation; completing the coverage is tracked in External-call: end-to-end and negative-path integration tests (post-merge follow-up for #549-#556) #564.

Requests without recorded external-call results short-circuit before any topology or network access, so the feature remains inert on protocol versions without external-call support and unless an extension service is configured.

Out of scope (deliberate, per the agreed short-cut)

Replay disagreements within a single reinterpretation already surface as model-conformance errors through the merged #552 machinery, independently of this check.

Coordination

With this PR the feature is functional end-to-end on ProtocolVersion.dev. The Daml-side PR (digital-asset/daml#23099) can proceed once a Canton snapshot/artifact containing this PR is published, per the coordination steps in #513.

Refs #513.

angelol added 3 commits July 7, 2026 11:51
…t views

The check moves to doParallelChecks, where view validation results do not
exist yet; the checker only ever read the views. Also renames the shared
test fixture to otherExternalCallResult (the deferrable MINOR from the
digital-asset#554 approval, discussion r3529116181) and adds a participantView test
helper.
…onfirmation

Runs the external-call check as one of the parallel validation suites in
doParallelChecks (the ModelConformanceChecker pattern: kicked off there,
awaited when confirmation responses are created):

- consistency: occurrences of the same call recorded across the request
  must agree on their output; every visible disagreement is alarmed,
- re-validation: undisputed recorded outputs are re-executed against the
  configured extension service, once per distinct call.

A disagreement from either part rejects the request on behalf of all
hosted confirming parties, joining the factory's existing per-view verdict
chain; a recorded result that cannot be re-validated (for example, no
extension service configured) downgrades approvals to abstentions.
Distinguishing the affected checking parties per view is a tracked
follow-up; replay disagreements within a single reinterpretation already
surface as model-conformance errors.

Also threads the ExternalCallValidator binding through the participant
wiring (ParticipantNode, CantonSyncService, ConnectedSynchronizer,
SynchronizerConnectionsManager, TransactionProcessor).
Check outcomes (pass, consistency rejection with alarm, re-validation
mismatch, unable-to-validate, malformed-payloads gating) and their
translation into confirmation responses (whole-view rejection for all
hosted confirming parties, abstention instead of approval, plain
approval). Completing the coverage is tracked in digital-asset#564.
@matthiasS-da matthiasS-da self-requested a review July 7, 2026 08:16
@matthiasS-da matthiasS-da self-assigned this Jul 7, 2026
@matthiasS-da

Copy link
Copy Markdown

Start reviewing...

angelol added 3 commits July 7, 2026 12:50
Remove the follow-up note from the class doc and the concurrency remark from
the check method doc: the former needs no in-code reminder, the latter
describes how the method is used rather than the method itself.
… chain

Append the external-call abstention to the per-view verdicts and widen the
verdict selection to NonPositiveLocalVerdict, so LocalAbstain propagates
through the same collectFirst as the rejections (with every rejection ranked
ahead of it) instead of being patched onto the approval afterwards.
Pins the ordering the verdict-chain restructure establishes: a co-occurring
rejection wins over the external-call abstention for every view.
with ExternalCallValidationTestUtil {

protected val factory: ExampleTransactionFactory =
new ExampleTransactionFactory(versionOverride = Some(ProtocolVersion.dev))(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We shouldn't hardcode the protocol version as already pointed out in previous PRs.

I'm ok if you address that as a post-merge cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taking the post-merge offer — tracked in #564 (#564 (comment)), together with the other test completions.

(logEntry: LogEntry) => {
logEntry.shouldBeCantonErrorCode(
ExternalCallValidationError.ExternalCallResultDisagreementAlarm
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As a post-merge cleanup, we should call shouldBeCantonError to check the error message as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tracked in #564 as well (#564 (comment)).

angelol added 3 commits July 7, 2026 13:22
One match on the check result produces the external-call verdict (reject or
abstain) at the slot of the former rejections, per review. The abstention
thereby ranks like the rejection: co-occurring verdicts from the earlier
suites win, those from the later suites (including malformed ones, when
confirming parties are hosted) are shadowed. A test pins the new ordering and
the replay-rejection comment is adjusted for it.
Every disagreement with the extension service is alarmed, mirroring the
visible-inconsistency alarms, and a comment explains why alarming all of them
is not redundant with the factory's rejection (which reports only the first).
The drain's justification now points at constructResponsesForMalformedPayloads,
the branch on which nothing awaits the check result.
The verdict selection runs in two passes: any rejection in the per-view
verdicts wins before an abstention is considered, regardless of position.
Separate case entries in a single collectFirst would not achieve this, as
collectFirst tries the cases per element in list order. The replay-rejection
comment reverts to its original claim, which holds again.
@matthiasS-da matthiasS-da self-requested a review July 7, 2026 09:46
matthiasS-da
matthiasS-da previously approved these changes Jul 7, 2026

@matthiasS-da matthiasS-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM except for the post-merge cleanup requests.

Per review: without runValidation the check makes no validator calls and
returns an already-completed future, so the drain only matters should the
check ever become asynchronous on the malformed-payloads path.
matthiasS-da
matthiasS-da previously approved these changes Jul 7, 2026

@matthiasS-da matthiasS-da left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM subject to the agreed post-cleanup tasks.

@matthiasS-da

Copy link
Copy Markdown

@angelol there is a problem with formatting. Let me take care of it.

@matthiasS-da

Copy link
Copy Markdown

@angelol there is a problem with formatting. Let me take care of it.

@angelol I can't, as I don't have permissions. Please run sbt format, commit and push. Afterwards, I'll merge the PR.

@angelol

angelol commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Done — sbt format fixed an import-order slip in the test file (my per-module scalafmt runs missed the scalafix part; noted for next time). Pushed as 9eef119; the branch is quiet from my side. Thanks for the fast turnaround today!

@matthiasS-da matthiasS-da self-requested a review July 7, 2026 11:03
@matthiasS-da matthiasS-da enabled auto-merge (squash) July 7, 2026 11:03
@matthiasS-da matthiasS-da merged commit 39f8740 into digital-asset:main Jul 7, 2026
21 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants