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

feat(solver/app): basic event processor #2386

Merged
merged 1 commit into from
Nov 6, 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
182 changes: 182 additions & 0 deletions solver/app/bindings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package solver

import (
"github.com/omni-network/omni/contracts/bindings"
"github.com/omni-network/omni/lib/errors"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

const (
statusInvalid uint8 = 0
statusPending uint8 = 1
statusAccepted uint8 = 2
statusRejected uint8 = 3
statusReverted uint8 = 4
statusFulfilled uint8 = 5
statusClaimed uint8 = 6
)

var (
inboxABI = mustGetABI(bindings.SolveInboxMetaData)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: use the Golang init function pattern instead of mustGet*

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

init should be avoided at all costs I think


// Event log topics (common.Hash).
topicRequested = mustGetEventTopic(inboxABI, "Requested")
topicAccepted = mustGetEventTopic(inboxABI, "Accepted")
topicRejected = mustGetEventTopic(inboxABI, "Rejected")
topicReverted = mustGetEventTopic(inboxABI, "Reverted")
topicFulfilled = mustGetEventTopic(inboxABI, "Fulfilled")
topicClaimed = mustGetEventTopic(inboxABI, "Claimed")
)

// eventMeta contains metadata about an event.
type eventMeta struct {
Topic common.Hash
Status uint8
ParseID func(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error)
}

var (
allEvents = []eventMeta{
{
Topic: topicRequested,
Status: statusPending,
ParseID: parseRequested,
},
{
Topic: topicAccepted,
Status: statusAccepted,
ParseID: parseAccepted,
},
{
Topic: topicRejected,
Status: statusRejected,
ParseID: parseRejected,
},
{
Topic: topicReverted,
Status: statusReverted,
ParseID: parseReverted,
},
{
Topic: topicFulfilled,
Status: statusFulfilled,
ParseID: parseFulfilled,
},
{
Topic: topicClaimed,
Status: statusClaimed,
ParseID: parseClaimed,
},
}

// eventsByTopic maps event topics to their metadata.
eventsByTopic = func() map[common.Hash]eventMeta {
resp := make(map[common.Hash]eventMeta, len(allEvents))
for _, e := range allEvents {
resp[e.Topic] = e
}

return resp
}()
)

func statusString(status uint8) string {
switch status {
case statusInvalid:
return "invalid"
case statusPending:
return "pending"
case statusAccepted:
return "accepted"
case statusRejected:
return "rejected"
case statusReverted:
return "reverted"
case statusFulfilled:
return "fulfilled"
case statusClaimed:
return "claimed"
default:
return "unknown"
}
}

func parseRequested(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseRequested(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse requested")
}

return e.Id, nil
}

func parseAccepted(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseAccepted(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse accepted")
}

return e.Id, nil
}

func parseRejected(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseRejected(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse rejected")
}

return e.Id, nil
}

func parseReverted(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseReverted(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse reverted")
}

return e.Id, nil
}

func parseFulfilled(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseFulfilled(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse fulfilled")
}

return e.Id, nil
}

func parseClaimed(contract bindings.SolveInboxFilterer, log types.Log) ([32]byte, error) {
e, err := contract.ParseClaimed(log)
if err != nil {
return [32]byte{}, errors.Wrap(err, "parse claimed")
}

return e.Id, nil
}

// mustGetABI returns the metadata's ABI as an abi.ABI type.
// It panics on error.
func mustGetABI(metadata *bind.MetaData) *abi.ABI {
abi, err := metadata.GetAbi()
if err != nil {
panic(err)
}

return abi
}

// mustGetEvent returns the event with the given name from the ABI.
// It panics if the event is not found.
func mustGetEventTopic(abi *abi.ABI, name string) common.Hash {
event, ok := abi.Events[name]
if !ok {
panic("event not found")
}

return event.ID
}
74 changes: 74 additions & 0 deletions solver/app/processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package solver

import (
"context"

"github.com/omni-network/omni/contracts/bindings"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/log"
"github.com/omni-network/omni/lib/xchain"

"github.com/ethereum/go-ethereum/core/types"
)

// procDeps abstracts dependencies for the event processor allowed simplified testing.
type procDeps struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this should be an interface, interface is used to do exactly what the comment says "abstracts dependencies for ... allowed simplified testing"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it is a style thing, I prefer funcs, since the implementations are not related, this allows better decoupling,

ParseID func(log types.Log) ([32]byte, error)
GetRequest func(ctx context.Context, chainID uint64, id [32]byte) (bindings.SolveRequest, bool, error)
ShouldReject func(ctx context.Context, chainID uint64, req bindings.SolveRequest) (string, bool, error)

Accept func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error
Reject func(ctx context.Context, chainID uint64, req bindings.SolveRequest, reason string) error
Fulfill func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error
Claim func(ctx context.Context, chainID uint64, req bindings.SolveRequest) error
}

// newEventProcessor returns a callback provided to xchain.Provider::StreamEventLogs processing
// all inbox contract events and driving request lifecycle.
func newEventProcessor(deps procDeps, chainID uint64) xchain.EventLogsCallback {
return func(ctx context.Context, _ uint64, elogs []types.Log) error {
for _, elog := range elogs {
event, ok := eventsByTopic[elog.Topics[0]]
if !ok {
return errors.New("unknown event [BUG]")
}

reqID, err := deps.ParseID(elog)
if err != nil {
return errors.Wrap(err, "parse id")
}

ctx := log.WithCtx(ctx, log.Hex7("req_id", reqID[:]))

req, _, err := deps.GetRequest(ctx, chainID, reqID)
if err != nil {
return errors.Wrap(err, "current status")
} else if event.Status != req.Status {
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to also check for case where event status is further than request status and mark it as a bug? so event status is fullffilled but request status is pending, because if this happens (if it can) we might miss a bug here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, good point, I'll add this in next PR

log.Info(ctx, "Ignoring mismatching old event", "actual", statusString(req.Status), "event", statusString(event.Status))
continue
}

switch event.Status {
case statusPending:
reason, reject, err := deps.ShouldReject(ctx, chainID, req)
if err != nil {
return errors.Wrap(err, "should reject")
} else if reject {
return deps.Reject(ctx, chainID, req, reason)
}

return deps.Accept(ctx, chainID, req)
Copy link
Contributor

@kc1116 kc1116 Nov 6, 2024

Choose a reason for hiding this comment

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

Can you explain why we don't Fulfill at this step?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

First Accept, if that succeeds, then fulfil

case statusAccepted:
return deps.Fulfill(ctx, chainID, req)
case statusFulfilled:
return deps.Claim(ctx, chainID, req)
case statusRejected, statusReverted, statusClaimed:
// Ignore for now
default:
return errors.New("unknown status [BUG]")
}
}

return nil
}
}
Loading
Loading