Skip to content

Cappl 1076 - #328

Merged
ettec merged 4 commits into
mainfrom
CAPPL-1076
Nov 6, 2025
Merged

Cappl 1076#328
ettec merged 4 commits into
mainfrom
CAPPL-1076

Conversation

@ettec

@ettec ettec commented Oct 24, 2025

Copy link
Copy Markdown
Contributor

PR to fast fail consensus requests when it is determined that it is not possible for the request to succeed. This allows for improved UX of the capability, but importantly also prevents a DoS attack vector.

In summary the approach taken is as follows:

Observation Phase:

All observations from the Observations phase for each request are passed to the Outcome phase, regardless of whether or not the observation is 'valid'. This allows the outcome to determine when >= 2f+1 requests have been. It follows that there must be at least f+1 valid observations amongst the received set.

Outcome Phase:

Once >= 2f+1 observations have been received the outcome phase attempts to achieve consensus across the observations, an outcome is created in both the success and failure cases which allows more timely processing of requests in the failure case and improves the UX with more meaningful failure messages returned to the workflow.

Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/value_consensus_test.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated

@nolag nolag 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.

This all feels over complicated.

Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go
Comment thread consensus/oracle/plugin/plugin_observation.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
@ettec

ettec commented Oct 27, 2025

Copy link
Copy Markdown
Contributor Author

This all feels over complicated.

I think it will simplify greatly depending on the answer to my question regards identical consensus. From what I understand we are saying when there are 2f+1 valid observations and still not f+1 matches identical consensus failed, even if with later arrived observations it would have met the f+1 criteria then it should mean minimal change to consensus_execution.

The above being the case, would this argument also apply to slices and maps, seems like it would? So in essence any user of identical across primitives, slices and maps needs to ensure that the source of the observations across the nodes would reasonably be expected to match and not meaningfully volatile (use median for volatile).

Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
@ettec
ettec force-pushed the CAPPL-1076 branch 4 times, most recently from 6afec93 to e204ea3 Compare October 29, 2025 16:13
@ettec
ettec marked this pull request as ready for review October 29, 2025 16:13
@ettec
ettec requested review from a team as code owners October 29, 2025 16:13
@ettec
ettec enabled auto-merge October 29, 2025 16:30
Comment thread consensus/oracle/plugin/batching_test.go Outdated
Comment thread consensus/oracle/plugin/batching_test.go Outdated
Comment thread consensus/oracle/plugin/plugin_observation.go Outdated
Comment thread consensus/oracle/plugin/plugin_observation.go Outdated
Comment thread consensus/oracle/plugin/batching.go Outdated
Comment thread consensus/oracle/plugin/plugin_observation.go Outdated
Comment thread consensus/oracle/plugin/plugin_observation.go Outdated
Comment thread consensus/oracle/types/value_consensus_types.proto Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/consensus_execution.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_reports.go
mchain0
mchain0 previously approved these changes Nov 3, 2025
@ettec
ettec force-pushed the CAPPL-1076 branch 2 times, most recently from f15f62f to 49a7aa6 Compare November 3, 2025 12:45
Comment thread consensus/oracle/plugin/batching/outcome_batch.go
Comment on lines +146 to +159
mddBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(&oracletypes.RequestObservation{
Metadata: obs.Metadata,
Input: &sdk.SimpleConsensusInputs{
Descriptors: obs.Input.Descriptors,
Default: obs.Input.Default,
},
})
if err != nil {
r.lggr.Errorw("could not marshal RequestObservation", "error", err)
continue
}

existingOutcomeSize = newSize
// Wrapped here to allow reuse of the existing CalculateOutcomeForObservations function for identical aggregation
allObservationsMDDBytes = append(allObservationsMDDBytes, values.Proto(values.NewBytes(mddBytes)))

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.

Can't you wrap the RequestObservation instead of marshalling it to bytes, then wrapping the bytes?

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.

possible, but looking at the logic that would be used in values.Wrap -> CreateMapFromStruct(v any) and then the map comparison logic in CalculateOutcomeForObservations, it's going to be significantly more efficient to compare the serialised proto bytes.

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.

I commented back on the new revision with a better way to do it. Also, unsure why you think it would be more efficient to serialize, they both use reflection.

@ettec ettec Nov 4, 2025

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.

unsure why you think it would be more efficient to serialize

The map comparison is going to identical check each attribute of the map versus just a []byte comparison, why would we want to do that? I don't see any good reason to do it that way

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.

I commented back on the new revision with a better way to do it

Replied there

Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
Comment thread consensus/oracle/plugin/duplicate_outcomes_test.go Outdated
Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
cachedObsSize = newSize
hasCapacity := observationBatch.AddObservation(ctx, reqObs)
if !hasCapacity {
break

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.

I left a comment in outcome about this, same feeling here.

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.

(responded to previous)

Comment thread consensus/oracle/transmitter/transmitter.go
Comment thread consensus/oracle/types/generate/main.go Outdated
}

message RequestOutcome {
message ConsensusFailedOutcome {

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.

For future reader's context:

We are in private beta and will have a maintenance window that we can deploy this in. It's better than complicating the plugin to support both.

@ettec
ettec force-pushed the CAPPL-1076 branch 2 times, most recently from 0edde8a to 5715ceb Compare November 4, 2025 13:24
func (r *reportingPlugin) calculateConsensusMetadataDescriptorAndDefault(observations []*oracletypes.RequestObservation) (*oracletypes.RequestObservation, error) {
var allObservationsMDDBytes []*valuespb.Value
for _, obs := range observations {
mddBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(&oracletypes.RequestObservation{

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.

Don't serialize this then wrap they bytes. Simply wrap it with values.Wrap then use the proto as the observation.

Alternatively, to be more efficient, refactor handleIdenticalAggregation to have a helper that takes a slice of proto.Message, then serialize them there. That way, serialization logic is in one place, and you can use RequestObservation directly and the return value will be from the candidate so you won't need to deserialize again.

@ettec ettec Nov 5, 2025

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.

Simply wrap it with values.Wrap

This will result in it being compared as a map, which will be obviously be less performant (but perhaps not in a way that matters), but it would be possible in that scenario to get a consensus mdd that does not contain values for some attributes as the map identical consensus returns the sub-set of attributes that match, so you would get a resultant consensus mdd with some attributes not set, when it should not be returning any consensus mdd at all.

Alternatively, to be more efficient, refactor handleIdenticalAggregation

This means modifying code designed to handle user supplied values to handle value types that would only be supplied by the plugin implementation. The sole advantage over the current approach is that it would avoid a single deserialization per request, given the outcome phase will typically deserialise ~10 times per request at least (depending on node count, and thats observations including the value, unlike here where its just mdd data) - I don't think this is justified, IMO the current approach is simple and avoids adding additional complication to the aggregation code.

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.

Ok, I'm unsure of the perf impact in that case, so maybe you're right. It should all be negligible, I won't hold this PR up longer.

Comment thread consensus/oracle/plugin/plugin_outcome.go Outdated
@@ -28,6 +28,7 @@ type ContractTransmitter struct {

func (c *ContractTransmitter) Transmit(ctx context.Context, configDigest types.ConfigDigest, seqNr uint64,

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.

With everything going on, it would be a lot easier if we didn't make the plugin return []byte for the info. Just pass the a strongly typed struct.

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.

(see reply to previous comment on this)

@cl-sonarqube-production

Copy link
Copy Markdown

return nil, fmt.Errorf("failed to serialise batch of request ids: %w", err)
}

qb.lggr.Debugw("serialised batch of request ids", "numRequests", len(qb.RequestIDs),

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.

nit: it would be helpful to log a full list of IDs because then we can trace it throughout the plugin execution. Same for all other phases.

message RequestOutcome {
message ConsensusOutcome {
oneof outcome {
ConsensusSuccessOutcome success = 1;

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.

Can you add a comment clarifying what can cause failure here? For example if all nodes pass the same error message as observation, will consensus end with a failure or not? If it does, then how can we distinguish failures caused by the user from our own failures? In other words, which metric should we base our dashboards and alerts on?

metrics metrics

outctx ocr3types.OutcomeContext

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.

nit: why so many empty lines?

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.

I have a weakness for whitespace :) - I'll sort it

err := proto.Unmarshal(query, requestsQuery)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to unmarshal query: %w", err)

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.

Can't we continue and still send all observations? That would prevent any stalls by buggy/malicious leader.

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.

Assuming this comment was meant for the observation phase? In the outcome phase the only option would be to ignore it (effectively current behaviour) or fail all requests which would be a worse outcome. Plus, by the time you are the observation phase the query has been validated in the observation phase, so to that.....

In the observation phase: there is a reasonable chance that request id order would be different across the nodes meaning consensus on the observations would probably not be reached. Also, if a leader is buggy/malicious they could send a list of invalid IDs so this wouldn't address that issue.


if len(errors) >= r.f+1 {
consensusFailedMsg := fmt.Sprintf(
"consensus calculation failed: received %d errors which is >= f+1 (%d) for requestID %s\nconsensus metadata, descriptor and default: %+v\nerrors received: %+v",

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.

nit: probably better to avoid newline characters

@ettec
ettec added this pull request to the merge queue Nov 6, 2025
Merged via the queue into main with commit 0a27236 Nov 6, 2025
18 checks passed
@ettec
ettec deleted the CAPPL-1076 branch November 6, 2025 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants