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

Ops relay upgrade #33

Merged
merged 14 commits into from
Mar 25, 2024
8 changes: 5 additions & 3 deletions auction/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type Auction struct {
open bool
userOpHash common.Hash

userOp *operation.UserOperation
solverOps []*operation.SolverOperation
userOp *operation.UserOperation
solverInput *operation.SolverInput
solverOps []*operation.SolverOperation

completionSubs []chan []*operation.SolverOperation

Expand All @@ -29,11 +30,12 @@ type Auction struct {
mu sync.RWMutex
}

func NewAuction(duration time.Duration, userOp *operation.UserOperation, userOpHash common.Hash) *Auction {
func NewAuction(duration time.Duration, userOp *operation.UserOperation, solverInput *operation.SolverInput, userOpHash common.Hash) *Auction {
auction := &Auction{
open: true,
userOpHash: userOpHash,
userOp: userOp,
solverInput: solverInput,
solverOps: make([]*operation.SolverOperation, 0),
completionSubs: make([]chan []*operation.SolverOperation, 0),
createdAt: time.Now(),
Expand Down
24 changes: 13 additions & 11 deletions auction/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,53 +70,55 @@ func (am *Manager) auctionsCleaner() {
}
}

func (am *Manager) NewUserOperation(userOp *operation.UserOperation) (common.Hash, *relayerror.Error) {
func (am *Manager) NewUserOperation(userOp *operation.UserOperation, hints []common.Address) (common.Hash, *operation.SolverInput, *relayerror.Error) {
userOpHash, relayErr := userOp.Hash()
if relayErr != nil {
log.Info("failed to compute user operation hash", "err", relayErr.Message)
return common.Hash{}, relayErr
return common.Hash{}, nil, relayErr
}

if relayErr := userOp.Validate(am.ethClient, am.config.Contracts.Atlas, am.atlasDomainSeparator, am.config.Relay.Gas.MaxPerUserOperation); relayErr != nil {
log.Info("invalid user operation", "err", relayErr.Message, "userOpHash", userOpHash.Hex())
return common.Hash{}, relayErr
return common.Hash{}, nil, relayErr
}

pData, err := contract.SimulatorAbi.Pack("simUserOperation", *userOp)
if err != nil {
log.Info("failed to pack user operation", "err", err, "userOpHash", userOpHash.Hex())
return common.Hash{}, relayerror.ErrServerInternal
return common.Hash{}, nil, relayerror.ErrServerInternal
}

bData, err := am.ethClient.CallContract(context.Background(), ethereum.CallMsg{To: &am.config.Contracts.Simulator, Data: pData}, nil)
if err != nil {
log.Info("failed to call simulator contract", "err", err, "userOpHash", userOpHash.Hex())
return common.Hash{}, relayerror.ErrServerInternal
return common.Hash{}, nil, relayerror.ErrServerInternal
}

validOp, err := contract.SimulatorAbi.Unpack("simUserOperation", bData)
if err != nil {
log.Info("failed to unpack simUserOperation return data", "err", err, "userOpHash", userOpHash.Hex())
return common.Hash{}, relayerror.ErrServerInternal
return common.Hash{}, nil, relayerror.ErrServerInternal
}

if !validOp[0].(bool) {
result := validOp[1].(uint8)
validCallResult := validOp[2].(*big.Int)
log.Info("user operation failed simulation", "userOpHash", userOpHash.Hex(), "result", result, "validCallResult", validCallResult.String())
return common.Hash{}, ErrUserOpFailedSimulation.AddMessage(fmt.Sprintf("result: %d, validCallResult: %s", result, validCallResult.String()))
return common.Hash{}, nil, ErrUserOpFailedSimulation.AddMessage(fmt.Sprintf("result: %d, validCallResult: %s", result, validCallResult.String()))
}

am.mu.Lock()
defer am.mu.Unlock()

if _, alreadyStarted := am.auctions[userOpHash]; alreadyStarted {
log.Info("auction for this user operation has already started", "userOpHash", userOpHash.Hex())
return common.Hash{}, ErrAuctionAlreadyStarted
return common.Hash{}, nil, ErrAuctionAlreadyStarted
}

am.auctions[userOpHash] = NewAuction(am.config.Relay.Auction.Duration, userOp, userOpHash)
return userOpHash, nil
solverInput := operation.NewSolverInput(userOp, hints)

am.auctions[userOpHash] = NewAuction(am.config.Relay.Auction.Duration, userOp, solverInput, userOpHash)
return userOpHash, solverInput, nil
}

func (am *Manager) GetSolverOperations(userOpHash common.Hash, completionChan chan []*operation.SolverOperation) ([]*operation.SolverOperation, *relayerror.Error) {
Expand Down Expand Up @@ -151,7 +153,7 @@ func (am *Manager) NewSolverOperation(solverOp *operation.SolverOperation) *rela
return ErrAuctionNotFound
}

relayErr := solverOp.Validate(auction.userOp, am.config.Contracts.Atlas, am.atlasDomainSeparator, am.config.Relay.Gas.MaxPerSolverOperation)
relayErr := solverOp.Validate(auction.solverInput, am.config.Contracts.Atlas, am.atlasDomainSeparator, am.config.Relay.Gas.MaxPerSolverOperation)
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
if relayErr != nil {
log.Info("invalid solver operation", "err", relayErr.Message, "userOpHash", auction.userOpHash.Hex())
return relayErr
Expand Down
6 changes: 3 additions & 3 deletions core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ func writeResponseData(w http.ResponseWriter, data interface{}) {
}

func (api *Api) SubmitUserOperation(w http.ResponseWriter, r *http.Request) {
userOpRaw := &operation.UserOperationRaw{}
if relayErr := getPostRequestData(r, userOpRaw); relayErr != nil {
userOpWithHintsRaw := &operation.UserOperationWithHintsRaw{}
if relayErr := getPostRequestData(r, userOpWithHintsRaw); relayErr != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write(relayErr.Marshal())
return
}
userOpHash, relayErr := api.relay.submitUserOperation(userOpRaw.Decode())
userOpHash, relayErr := api.relay.submitUserOperation(userOpWithHintsRaw.Decode())
if relayErr != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write(relayErr.Marshal())
Expand Down
6 changes: 3 additions & 3 deletions core/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ func (r *Relay) Run(serverReadyChan chan struct{}) {
r.server.ListenAndServe(serverReadyChan)
}

func (r *Relay) submitUserOperation(userOp *operation.UserOperation) (common.Hash, *relayerror.Error) {
userOpHash, relayErr := r.auctionManager.NewUserOperation(userOp)
func (r *Relay) submitUserOperation(userOp *operation.UserOperation, hints []common.Address) (common.Hash, *relayerror.Error) {
userOpHash, solverInput, relayErr := r.auctionManager.NewUserOperation(userOp, hints)
if relayErr != nil {
return common.Hash{}, relayErr
}

go r.server.BroadcastUserOperation(userOp)
go r.server.BroadcastSolverInput(solverInput)
return userOpHash, nil
}

Expand Down
12 changes: 6 additions & 6 deletions core/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
MethodSubmitSolverOperation = "submitSolverOperation"

// Subscriptions topics
TopicNewUserOperations = "newUserOperations"
TopicnewSolverInputs = "newSolverInputs"
jj1980a marked this conversation as resolved.
Show resolved Hide resolved

// Events
EventUpdate = "update"
Expand Down Expand Up @@ -71,7 +71,7 @@ var (
}

Topics = map[string]struct{}{
TopicNewUserOperations: {},
TopicnewSolverInputs: {},
}

upgrader = websocket.Upgrader{
Expand Down Expand Up @@ -118,7 +118,7 @@ func (r *Response) Marshal() []byte {
}

type BroadcastParams struct {
UserOperation *operation.UserOperationRaw `json:"userOperation,omitempty"`
SolverInput *operation.SolverInput `json:"solverInput,omitempty"`
}

type Broadcast struct {
Expand Down Expand Up @@ -308,12 +308,12 @@ func (s *Server) unregisterBundler(conn *Conn) {
delete(s.bundlers, conn.bundler)
}

func (s *Server) BroadcastUserOperation(userOp *operation.UserOperation) {
func (s *Server) BroadcastSolverInput(solverInput *operation.SolverInput) {
broadcast := &Broadcast{
Event: EventUpdate,
Topic: TopicNewUserOperations,
Topic: TopicnewSolverInputs,
Data: &BroadcastParams{
UserOperation: userOp.EncodeToRaw(),
SolverInput: solverInput,
},
}
s.publish(broadcast)
Expand Down
66 changes: 62 additions & 4 deletions operation/solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package operation

import (
"math/big"
"math/rand"

relayCrypto "github.com/FastLane-Labs/atlas-operations-relay/crypto"
"github.com/FastLane-Labs/atlas-operations-relay/log"
Expand Down Expand Up @@ -68,6 +69,58 @@ var (
}
)

type SolverInput struct {
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
UserOpHash common.Hash `json:"userOpHash"`
To common.Address `json:"to"`
Gas hexutil.Big `json:"gas"`
MaxFeePerGas hexutil.Big `json:"maxFeePerGas"`
Deadline hexutil.Big `json:"deadline"`
Dapp common.Address `json:"dapp"`
Control common.Address `json:"control"`

//Exactly one of 1. Hints 2. (Value, Data, From) must be set
Hints []common.Address `json:"hints,omitempty"`

Value hexutil.Big `json:"value"`
Data hexutil.Bytes `json:"data,omitempty"`
From common.Address `json:"from"`
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
}

func NewSolverInput(userOp *UserOperation, hints []common.Address) *SolverInput {
userOpHash, _ := userOp.Hash()
solverInput := &SolverInput{
UserOpHash: userOpHash,
To: userOp.To,
Gas: hexutil.Big(*userOp.Gas),
MaxFeePerGas: hexutil.Big(*userOp.MaxFeePerGas),
Deadline: hexutil.Big(*userOp.Deadline),
Dapp: userOp.Dapp,
Control: userOp.Control,
}

if len(hints) > 0 {
//randomize hints
rand.Shuffle(len(hints), func(i, j int) { hints[i], hints[j] = hints[j], hints[i] })

solverInput.Hints = hints
} else {
solverInput.Data = hexutil.Bytes(userOp.Data)
solverInput.From = userOp.From
solverInput.Value = hexutil.Big(*userOp.Value)
}

return solverInput
}

func (si *SolverInput) Validate() error {
isHinted := si.Hints != nil && len(si.Hints) > 0
isDirect := si.Data != nil || si.From != common.Address{} || si.Value.ToInt().Cmp(big.NewInt(0)) > 0
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
if isHinted && isDirect {
return relayerror.NewError(2000, "solver input cannot contain both (hints) and (value or from or data) ")
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}

// External representation of a solver operation,
// the relay receives and broadcasts solver operations in this format
type SolverOperationRaw struct {
Expand Down Expand Up @@ -139,7 +192,7 @@ func (s *SolverOperation) EncodeToRaw() *SolverOperationRaw {
}
}

func (s *SolverOperation) Validate(userOp *UserOperation, atlas common.Address, atlasDomainSeparator common.Hash, gasLimit *big.Int) *relayerror.Error {
func (s *SolverOperation) Validate(solverInput *SolverInput, atlas common.Address, atlasDomainSeparator common.Hash, gasLimit *big.Int) *relayerror.Error {
if s.To != atlas {
return ErrSolverOpInvalidToField
}
Expand All @@ -148,15 +201,15 @@ func (s *SolverOperation) Validate(userOp *UserOperation, atlas common.Address,
return ErrSolverOpGasLimitExceeded
}

if s.MaxFeePerGas.Cmp(userOp.MaxFeePerGas) < 0 {
if s.MaxFeePerGas.Cmp(solverInput.MaxFeePerGas.ToInt()) < 0 {
return ErrSolverOpMaxFeePerGasTooLow
}

if s.Deadline.Cmp(userOp.Deadline) < 0 {
if s.Deadline.Cmp(solverInput.Deadline.ToInt()) < 0 {
return ErrSolverOpDeadlineTooLow
}

if s.Control != userOp.Control {
if s.Control != solverInput.Control {
return ErrSolverOpDAppControlMismatch
}

Expand Down Expand Up @@ -211,6 +264,11 @@ func (s *SolverOperation) ProofHash() (common.Hash, error) {
}

func (s *SolverOperation) checkSignature(domainSeparator common.Hash) *relayerror.Error {
if len(s.Signature) != 65 {
log.Info("invalid solver operation signature length", "length", len(s.Signature))
return ErrSolverOpSignatureInvalid
}

proofHash, err := s.ProofHash()
if err != nil {
log.Info("failed to compute solver proof hash", "err", err)
Expand Down
21 changes: 21 additions & 0 deletions operation/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ func (u *UserOperationRaw) Decode() *UserOperation {
}
}

type UserOperationWithHintsRaw struct {
UserOperation *UserOperationRaw `json:"userOperation"`
Hints []common.Address `json:"hints,omitempty"`
}

func NewUserOperationWithHintsRaw(userOp *UserOperationRaw, hints []common.Address) *UserOperationWithHintsRaw {
return &UserOperationWithHintsRaw{
UserOperation: userOp,
Hints: hints,
}
}

func (uop *UserOperationWithHintsRaw) Decode() (*UserOperation, []common.Address) {
return uop.UserOperation.Decode(), uop.Hints
}

// Internal representation of a user operation
type UserOperation struct {
From common.Address
Expand Down Expand Up @@ -211,6 +227,11 @@ func (u *UserOperation) ProofHash() (common.Hash, error) {
}

func (u *UserOperation) checkSignature(domainSeparator common.Hash) *relayerror.Error {
if len(u.Signature) != 65 {
log.Info("invalid user operation signature length", "length", len(u.Signature))
return ErrUserOpInvalidSignature
}

jj1980a marked this conversation as resolved.
Show resolved Hide resolved
proofHash, err := u.ProofHash()
if err != nil {
log.Info("failed to compute user proof hash", "err", err)
Expand Down
2 changes: 1 addition & 1 deletion specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The operations relay for Atlas serves as an infrastructure layer facilitating co
## Lifecycle

1. The frontend calls `/userOperation` endpoint to submit a new user operation. The endpoint returns the `userOpHash` in case of success, or an error.
2. The relay broadcasts the user operation to solvers via websocket (solvers connect to the relay and subscribe to the `newUserOperations` topic). When the operation is broadcast, the relay must also start the auction timer for this operation.
2. The relay broadcasts the user operation to solvers via websocket (solvers connect to the relay and subscribe to the `newSolverInputs` topic). When the operation is broadcast, the relay must also start the auction timer for this operation.
3. Solvers send solver operations to the relay in response to the user operation (via REST or websocket API).
4. The relay stops collecting solver operations after the auction timer hits 500ms.
5. The frontend calls the `/solverOperations` endpoint, specifying the `userOpHash` as a parameter, to retrieve the solver operations tied to a particular user operation. If it calls this endpoint before the end of the auction duration (500ms), the endpoint returns an error. If the frontend sets the `wait` parameter to `true` when calling, the relay will hold the request until the auction’s completion then return the results.
Expand Down
12 changes: 6 additions & 6 deletions specs/ws-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ components:
id: 1
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
method: subscribe
params:
topic: newUserOperations
topic: newSolverInputs
x-response:
anyOf:
- $ref: '#/components/messages/Success'
Expand All @@ -158,7 +158,7 @@ components:
id: 1
method: unsubscribe
params:
topic: newUserOperations
topic: newSolverInputs
x-response:
anyOf:
- $ref: '#/components/messages/Success'
Expand Down Expand Up @@ -248,7 +248,7 @@ components:
properties:
topic:
type: string
enum: ['newUserOperations']
enum: ['newSolverInputs']
required:
- id
- method
Expand All @@ -267,7 +267,7 @@ components:
properties:
topic:
type: string
enum: ['newUserOperations']
enum: ['newSolverInputs']
required:
- id
- method
Expand All @@ -281,12 +281,12 @@ components:
const: update
topic:
type: string
enum: ['newUserOperations']
enum: ['newSolverInputs']
data:
type: object
properties:
userOperation:
$ref: '#/components/schemas/UserOperation'
$ref: '#/components/schemas/SolverInput'
jj1980a marked this conversation as resolved.
Show resolved Hide resolved
required:
- event
- topic
Expand Down
Loading
Loading