feat(external-call): minimal protocol integration#567
Conversation
…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.
|
Start reviewing... |
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))( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Taking the post-merge offer — tracked in #564 (#564 (comment)), together with the other test completions.
| (logEntry: LogEntry) => { | ||
| logEntry.shouldBeCantonErrorCode( | ||
| ExternalCallValidationError.ExternalCallResultDisagreementAlarm | ||
| ) |
There was a problem hiding this comment.
As a post-merge cleanup, we should call shouldBeCantonError to check the error message as well.
There was a problem hiding this comment.
Tracked in #564 as well (#564 (comment)).
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
LGTM subject to the agreed post-cleanup tasks.
|
@angelol there is a problem with formatting. Let me take care of it. |
|
Done — |
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_CALLbuiltin 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 fromdoParallelChecksinTransactionProcessingSteps, following theModelConformanceChecker.checkpattern (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.LOCAL_VERDICT_EXTERNAL_CALL_RESULT_DISAGREEMENT); a recorded result that cannot be re-validated (e.g. no extension service configured) downgrades approvals toCANNOT_PERFORM_ALL_VALIDATIONSabstentions. 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).doParallelCheckstime), and theExternalCallValidatorbinding is threaded through the participant wiring.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.