Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec plugin: Introduce a State Machine #63

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions execute/exectypes/outcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,47 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"
)

type PluginState string

const (
// Unknown is the zero value, this allows a "Next" state transition for uninitialized values (i.e. the first round).
Unknown PluginState = ""

// GetCommitReports is the first step, it is used to select commit reports from the destination chain.
GetCommitReports PluginState = "GetCommitReports"

// GetMessages is the second step, given a set of commit reports it fetches the associated messages.
GetMessages PluginState = "GetMessages"

// Filter is the final step, any additional destination data is collected to complete the execution report.
Filter PluginState = "Filter"
)

// Next returns the next state for the plugin. The Unknown state is used to transition from uninitialized values.
func (p PluginState) Next() PluginState {
switch p {
case GetCommitReports:
return GetMessages

case GetMessages:
// TODO: go to Filter after GetMessages
return GetCommitReports

case Unknown:
Copy link
Contributor

Choose a reason for hiding this comment

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

// Unknown is the default value indicating there was no previous state.

If there was no previous state, should it start getting new messages? Are we going to start the plugin state with Unknown? If we expect to start with it, why fallthrough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically, I chose to do it this way so that an uninitialized previous outcome (i.e. the first round after startup) can transition to the "Next" state with no special handling. So you can get to the first state from Unknown or Filter (or GetMessages in this PR, since I haven't implemented Filter yet).

fallthrough
case Filter:
return GetCommitReports

default:
panic("unexpected execute plugin state")
}
}

// Outcome is the outcome of the ExecutePlugin.
type Outcome struct {
// State that the outcome was generated for.
State PluginState

// PendingCommitReports are the oldest reports with pending commits. The slice is
// sorted from oldest to newest.
PendingCommitReports []CommitData `json:"commitReports"`
Expand All @@ -17,28 +56,27 @@ type Outcome struct {
Report cciptypes.ExecutePluginReport `json:"report"`
}

// IsEmpty returns true if the outcome has no pending commit reports or chain reports.
func (o Outcome) IsEmpty() bool {
return len(o.PendingCommitReports) == 0 && len(o.Report.ChainReports) == 0
}

// NewOutcome creates a new Outcome with the pending commit reports and the chain reports sorted.
func NewOutcome(
state PluginState,
pendingCommits []CommitData,
report cciptypes.ExecutePluginReport,
) Outcome {
return newSortedOutcome(pendingCommits, report)
}

// Encode encodes the outcome by first sorting the pending commit reports and the chain reports
// and then JSON marshalling.
// The encoding MUST be deterministic.
func (o Outcome) Encode() ([]byte, error) {
// We sort again here in case construction is not via the constructor.
return json.Marshal(newSortedOutcome(o.PendingCommitReports, o.Report))
return newSortedOutcome(state, pendingCommits, report)
}

// newSortedOutcome ensures canonical ordering of the outcome.
// TODO: handle canonicalization in the encoder.
func newSortedOutcome(
state PluginState,
pendingCommits []CommitData,
report cciptypes.ExecutePluginReport) Outcome {
report cciptypes.ExecutePluginReport,
) Outcome {
pendingCommitsCP := append([]CommitData{}, pendingCommits...)
reportCP := append([]cciptypes.ExecutePluginReportSingleChain{}, report.ChainReports...)
sort.Slice(
Expand All @@ -52,11 +90,21 @@ func newSortedOutcome(
return reportCP[i].SourceChainSelector < reportCP[j].SourceChainSelector
})
return Outcome{
State: state,
PendingCommitReports: pendingCommitsCP,
Report: cciptypes.ExecutePluginReport{ChainReports: reportCP},
}
}

// Encode encodes the outcome by first sorting the pending commit reports and the chain reports
// and then JSON marshalling.
// The encoding MUST be deterministic.
func (o Outcome) Encode() ([]byte, error) {
// We sort again here in case construction is not via the constructor.
return json.Marshal(newSortedOutcome(o.State, o.PendingCommitReports, o.Report))
}

// DecodeOutcome decodes the outcome from JSON.
func DecodeOutcome(b []byte) (Outcome, error) {
o := Outcome{}
err := json.Unmarshal(b, &o)
Expand Down
53 changes: 53 additions & 0 deletions execute/exectypes/outcome_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package exectypes

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestPluginState_Next(t *testing.T) {
tests := []struct {
name string
p PluginState
want PluginState
isPanic bool
}{
{
name: "Zero value",
p: Unknown,
want: GetCommitReports,
},
{
name: "Phase 1 to 2",
p: GetCommitReports,
want: GetMessages,
},
{
name: "Phase 2 to 1",
p: GetMessages,
want: GetCommitReports,
},
{
name: "panic",
p: PluginState("ElToroLoco"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

🤠

isPanic: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.isPanic {
require.Panics(t, func() {
tt.p.Next()
})
return
}

if got := tt.p.Next(); got != tt.want {
t.Errorf("Next() = %v, want %v", got, tt.want)
}
})
}
}
Loading
Loading