Skip to content
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
3 changes: 3 additions & 0 deletions chain_capabilities/solana/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

solgo "github.com/gagliardetto/solana-go"
"google.golang.org/protobuf/proto"

"github.com/smartcontractkit/capabilities/chain_capabilities/solana/metering"
Expand Down Expand Up @@ -53,6 +54,7 @@ type Solana struct {
messageBuilder *monitoring.MessageBuilder
transmissionScheduler ts.TransmissionScheduler
handler chainconsensus.RequestHandler
forwarderState solgo.PublicKey
}

func NewSolana(ctx context.Context, cfg *config.Config, s types.SolanaService, messageBuilder *monitoring.MessageBuilder,
Expand All @@ -76,6 +78,7 @@ func NewSolana(ctx context.Context, cfg *config.Config, s types.SolanaService, m
beholderProcessor: beholderProcessor,
transmissionScheduler: transmissionScheduler,
handler: handler,
forwarderState: cfg.CREForwarderState,
}

return sol, sol.initLimiters(limitsFactory)
Expand Down
7 changes: 2 additions & 5 deletions chain_capabilities/solana/actions/forwarder_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ func newForwarderClient(solService types.SolanaService, lggr logger.Logger, forw

func (fc *forwarderClient) InvokeOnReport(ctx context.Context, receiver solana.PublicKey, meta []*solcap.AccountMeta,
report *sdk.ReportResponse, gasConfig *solcap.ComputeConfig) (*soltypes.SubmitTransactionReply, error) {
if len(meta) < 2 {
Comment thread
amit-momin marked this conversation as resolved.
return nil, fmt.Errorf("expected accounts meta length > 2, got: %d", len(meta))
}
reportMetadata, _, err := ocr3types.Decode(report.RawReport)
if err != nil {
return nil, fmt.Errorf("failed to decode report metadata: %w", err)
Expand Down Expand Up @@ -163,11 +160,11 @@ func (fc *forwarderClient) getOracleConfigPDA(ctx context.Context, workflowDonID
},
})
if err != nil {
return oracleConfigPDA, fmt.Errorf("error fetching cache state account %v; err: %w", oracleConfigPDA, err)
return oracleConfigPDA, fmt.Errorf("error fetching config PDA account %v; err: %w", oracleConfigPDA, err)
}

if oracleConfigAccount.Value == nil {
return oracleConfigPDA, fmt.Errorf("cache state account does not exist %v", oracleConfigPDA)
return oracleConfigPDA, fmt.Errorf("config PDA account does not exist %v", oracleConfigPDA)
}

return oracleConfigPDA, nil
Expand Down
74 changes: 67 additions & 7 deletions chain_capabilities/solana/actions/write_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
Expand Down Expand Up @@ -77,10 +78,16 @@ func (s *Solana) WriteReport(
telemetryContext := monitoring.TelemetryContext{TsStart: time.Now().UnixMilli(), RequestMetadata: metadata}
monitoring.EmitInitiated(ctx, s.lggr, s.beholderProcessor, s.messageBuilder.BuildWriteReportInitiated(telemetryContext, input))
// 1. Validate inputs
err := s.validateInputsAndReportMetadata(metadata, input)
err := s.validateInputsAndReportMetadata(ctx, metadata, input)
if err != nil {
monitoring.LogAndEmitError(ctx, s.lggr, s.beholderProcessor, s.messageBuilder.BuildWriteReportError(telemetryContext, input, "Failed to WriteReport, user error due to invalid request", err.Error(), true))
return nil, NewUserError(err)
// Since we check if the receiver is executable during inputs validation, it's possible we failed the rpc call to get receiver's account info
if !errors.Is(err, ErrRPC) {
return nil, NewUserError(err)
}

return nil, GetError(err, false)

}

report, billingMetadata, err := s.executeWriteReport(ctx, input, metadata, telemetryContext)
Expand Down Expand Up @@ -248,7 +255,7 @@ func (s *Solana) isUserErrorWriteReport(err error) bool {
return strings.HasPrefix(err.Error(), capcommon.UserError)
}

func (s *Solana) validateInputsAndReportMetadata(requestMetadata capabilities.RequestMetadata, request *solcap.WriteReportRequest) error {
func (s *Solana) validateInputsAndReportMetadata(ctx context.Context, requestMetadata capabilities.RequestMetadata, request *solcap.WriteReportRequest) error {
if request == nil {
return errors.New("nil WriteReportRequest")
}
Expand All @@ -261,9 +268,6 @@ func (s *Solana) validateInputsAndReportMetadata(requestMetadata capabilities.Re
if key := solana.PublicKey(request.Receiver); key.IsZero() {
return fmt.Errorf("receiver public key is empty")
}
if err := validateRemainingAccountMetas(request.GetRemainingAccounts()); err != nil {
return err
}
if len(request.Report.Sigs) == 0 {
return fmt.Errorf("no signatures provided")
}
Expand Down Expand Up @@ -311,10 +315,66 @@ func (s *Solana) validateInputsAndReportMetadata(requestMetadata capabilities.Re
return fmt.Errorf("workflowID in the report does not match WorkflowID in the request metadata. Report WorkflowID: %s, request WorkflowID: %s", reportMetadata.WorkflowID, requestMetadata.WorkflowID)
}

err = validateRemainingAccountsHash(request.RemainingAccounts, request.Report.RawReport)
err = s.validateWriteReportPayload(ctx, request.Receiver, request.RemainingAccounts, request.Report.RawReport)
if err != nil {
return fmt.Errorf("report payload is invalid: %w", err)
}
return nil
}

// ErrRPC marks a validation failure caused by an unreachable RPC rather than by the request itself.
var ErrRPC = errors.New("rpc call failed")

// validateReportPayload performs most of onchain
// 1. validates that remaining accounts hash matches
// 2. validates that there is enough remainings accounts
// 3. validates that passed forwarderState aligned with capability config
// 4. validates that the receiver is valid address, exists and is executable solana program
func (s *Solana) validateWriteReportPayload(ctx context.Context, receiver []byte, remainings []*solcap.AccountMeta, rawReport []byte) error {
err := validateRemainingAccountsHash(remainings, rawReport)
if err != nil {
return fmt.Errorf("failed to validate remaining account hash: %w", err)
}
if len(remainings) < 2 {
return fmt.Errorf("expected accounts meta length > 2, got: %d", len(remainings))
}
if err := validateRemainingAccountMetas(remainings); err != nil {
return err
}
forwarderState := solana.PublicKey(remainings[0].GetPublicKey())
if !forwarderState.Equals(s.forwarderState) {
return fmt.Errorf("forwarder state from remainings accounts list %s doesn't match configured forwarder state %s", forwarderState, s.forwarderState)
}
if len(receiver) != solana.PublicKeyLength {
return fmt.Errorf("received public key is not 32 bytes long. key in hex: %s", hex.EncodeToString(receiver))
}

var acc *soltypes.GetAccountInfoReply
acc, err = capcommon.WithQuickRetry(ctx, s.lggr, func(ctx context.Context) (*soltypes.GetAccountInfoReply, error) {
acc, err := s.SolanaService.GetAccountInfoWithOpts(ctx, soltypes.GetAccountInfoRequest{

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 could lead to a state where an unrelated RPC failure results in a "System error" when a report potentially is already on-chain. Are we ok with that tradeoff?

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 think if the node experiences RPC failure it won't be able to retreive transmissionState from logs as well. Having RPC failure here will probably be even easier to debug in this case (easier than "transmission state not found..")

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.

Hm, yeah thats fair.

Account: soltypes.PublicKey(receiver),
Opts: &soltypes.GetAccountInfoOpts{
Commitment: soltypes.CommitmentProcessed,
},
})
if errors.Is(err, rpc.ErrNotFound) {
// We handle nil account later, no need to retry if acc is missing
return nil, nil
}
return acc, err
})

if err != nil {
return fmt.Errorf("%w: failed to get receiver's account: %w", ErrRPC, err)
}

if acc == nil || acc.Value == nil {
return errors.New("receiver account does not exist")
}

if !acc.Value.Executable {
return errors.New("receiver account is non-executable")
}

return nil
}
Expand Down
Loading
Loading