From b69063b0c0967d9b97ec3dbe68175c56c56ff820 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 14:09:59 -0500 Subject: [PATCH 01/37] WIP: guard skeleton --- services/rfq/guard/service/guard.go | 154 ++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 services/rfq/guard/service/guard.go diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go new file mode 100644 index 0000000000..aaaaa7747a --- /dev/null +++ b/services/rfq/guard/service/guard.go @@ -0,0 +1,154 @@ +package guard + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ipfs/go-log" + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/ethergo/listener" + omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" + "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var logger = log.Logger("guard") + +// Guard monitors calls to prove() and verifies them. +type Guard struct { + cfg relconfig.Config + metrics metrics.Handler + chainListeners map[int]listener.ContractListener + contracts map[int]*fastbridge.FastBridgeRef +} + +// NewGuard creates a new Guard. +func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg relconfig.Config) (*Guard, error) { + omniClient := omniClient.NewOmnirpcClient(cfg.OmniRPCURL, metricHandler, omniClient.WithCaptureReqRes()) + chainListeners := make(map[int]listener.ContractListener) + + dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type) + if err != nil { + return nil, fmt.Errorf("could not get db type: %w", err) + } + store, err := connect.Connect(ctx, dbType, cfg.Database.DSN, metricHandler) + if err != nil { + return nil, fmt.Errorf("could not make db: %w", err) + } + + // setup chain listeners + contracts := make(map[int]*fastbridge.FastBridgeRef) + for chainID := range cfg.GetChains() { + rfqAddr, err := cfg.GetRFQAddress(chainID) + if err != nil { + return nil, fmt.Errorf("could not get rfq address: %w", err) + } + chainClient, err := omniClient.GetChainClient(ctx, chainID) + if err != nil { + return nil, fmt.Errorf("could not get chain client: %w", err) + } + + contract, err := fastbridge.NewFastBridgeRef(common.HexToAddress(rfqAddr), chainClient) + if err != nil { + return nil, fmt.Errorf("could not create fast bridge contract: %w", err) + } + startBlock, err := contract.DeployBlock(&bind.CallOpts{Context: ctx}) + if err != nil { + return nil, fmt.Errorf("could not get deploy block: %w", err) + } + chainListener, err := listener.NewChainListener(chainClient, store, common.HexToAddress(rfqAddr), uint64(startBlock.Int64()), metricHandler) + if err != nil { + return nil, fmt.Errorf("could not get chain listener: %w", err) + } + chainListeners[chainID] = chainListener + + // setup FastBridge contract on this chain + addr, err := cfg.GetRFQAddress(chainID) + if err != nil { + return nil, fmt.Errorf("could not get rfq address: %w", err) + } + contracts[chainID], err = fastbridge.NewFastBridgeRef(common.HexToAddress(addr), chainClient) + if err != nil { + return nil, fmt.Errorf("could not create bridge contract: %w", err) + } + } + + return &Guard{ + cfg: cfg, + chainListeners: chainListeners, + contracts: contracts, + }, nil +} + +// Start starts the guard. +func (g *Guard) Start(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + } + } + +} + +func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { + chainListener := g.chainListeners[chainID] + + parser, err := fastbridge.NewParser(chainListener.Address()) + if err != nil { + return fmt.Errorf("could not parse: %w", err) + } + + err = chainListener.Listen(ctx, func(parentCtx context.Context, log types.Log) (err error) { + et, parsedEvent, ok := parser.ParseEvent(log) + // handle unknown event + if !ok { + if len(log.Topics) != 0 { + logger.Warnf("unknown event %s", log.Topics[0]) + } + return nil + } + + ctx, span := g.metrics.Tracer().Start(parentCtx, fmt.Sprintf("handleLog-%s", et), trace.WithAttributes( + attribute.String(metrics.TxHash, log.TxHash.String()), + attribute.Int(metrics.Origin, chainID), + attribute.String(metrics.Contract, log.Address.String()), + attribute.String("block_hash", log.BlockHash.String()), + attribute.Int64("block_number", int64(log.BlockNumber)), + )) + + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + switch event := parsedEvent.(type) { + case *fastbridge.FastBridgeBridgeProofProvided: + err = g.handleProofProvidedLog(ctx, event, chainID) + if err != nil { + return fmt.Errorf("could not handle request: %w", err) + } + } + + return nil + }) + + if err != nil { + return fmt.Errorf("listener failed: %w", err) + } + return nil +} + +func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { + // contract, ok := g.contracts[chainID] + // if !ok { + // return fmt.Errorf("could not get contract for chain: %d", chainID) + // } + return nil +} From 1c4ef5f4a8bc651b54138991f1e7b08d1ea1cf95 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 14:54:17 -0500 Subject: [PATCH 02/37] WIP: add guarddb package --- services/rfq/guard/guarddb/base/doc.go | 2 + services/rfq/guard/guarddb/base/model.go | 111 ++++++++++++++++++ services/rfq/guard/guarddb/base/proven.go | 55 +++++++++ services/rfq/guard/guarddb/base/store.go | 44 +++++++ services/rfq/guard/guarddb/connect/sql.go | 39 ++++++ services/rfq/guard/guarddb/db.go | 99 ++++++++++++++++ services/rfq/guard/guarddb/doc.go | 3 + services/rfq/guard/guarddb/mysql/mysql.go | 63 ++++++++++ .../guarddb/pendingprovenstatus_string.go | 27 +++++ services/rfq/guard/guarddb/sqlite/sqlite.go | 62 ++++++++++ 10 files changed, 505 insertions(+) create mode 100644 services/rfq/guard/guarddb/base/doc.go create mode 100644 services/rfq/guard/guarddb/base/model.go create mode 100644 services/rfq/guard/guarddb/base/proven.go create mode 100644 services/rfq/guard/guarddb/base/store.go create mode 100644 services/rfq/guard/guarddb/connect/sql.go create mode 100644 services/rfq/guard/guarddb/db.go create mode 100644 services/rfq/guard/guarddb/doc.go create mode 100644 services/rfq/guard/guarddb/mysql/mysql.go create mode 100644 services/rfq/guard/guarddb/pendingprovenstatus_string.go create mode 100644 services/rfq/guard/guarddb/sqlite/sqlite.go diff --git a/services/rfq/guard/guarddb/base/doc.go b/services/rfq/guard/guarddb/base/doc.go new file mode 100644 index 0000000000..a693590881 --- /dev/null +++ b/services/rfq/guard/guarddb/base/doc.go @@ -0,0 +1,2 @@ +// Package base contains the base implementation for different sql driers. +package base diff --git a/services/rfq/guard/guarddb/base/model.go b/services/rfq/guard/guarddb/base/model.go new file mode 100644 index 0000000000..26a9cbbeac --- /dev/null +++ b/services/rfq/guard/guarddb/base/model.go @@ -0,0 +1,111 @@ +package base + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" +) + +func init() { + namer := dbcommon.NewNamer(GetAllModels()) + statusFieldName = namer.GetConsistentName("Status") + transactionIDFieldName = namer.GetConsistentName("TransactionID") + originTxHashFieldName = namer.GetConsistentName("OriginTxHash") + destTxHashFieldName = namer.GetConsistentName("DestTxHash") + rebalanceIDFieldName = namer.GetConsistentName("RebalanceID") + relayNonceFieldName = namer.GetConsistentName("RelayNonce") +} + +var ( + // statusFieldName is the status field name. + statusFieldName string + // transactionIDFieldName is the transactions id field name. + transactionIDFieldName string + // originTxHashFieldName is the origin tx hash field name. + originTxHashFieldName string + // destTxHashFieldName is the dest tx hash field name. + destTxHashFieldName string + // rebalanceIDFieldName is the rebalances id field name. + rebalanceIDFieldName string + // relayNonceFieldName is the relay nonce field name. + relayNonceFieldName string +) + +// PendingProvenModel is the primary event model. +type PendingProvenModel struct { + // CreatedAt is the creation time + CreatedAt time.Time + // UpdatedAt is the update time + UpdatedAt time.Time + // TransactionID is the transaction id of the event + TransactionID string `gorm:"column:transaction_id;primaryKey"` + // TxHash is the hash of the relay transaction on destination + TxHash string + // Status is the status of the event + Status guarddb.PendingProvenStatus +} + +// FromPendingProven converts a quote request to an object that can be stored in the db. +func FromPendingProven(proven guarddb.PendingProven) PendingProvenModel { + return PendingProvenModel{ + TransactionID: hexutil.Encode(proven.TransactionID[:]), + TxHash: proven.TxHash.Hex(), + Status: proven.Status, + } +} + +var emptyHash = common.HexToHash("").Hex() + +func hashToNullString(h common.Hash) sql.NullString { + if h.Hex() == emptyHash { + return sql.NullString{Valid: false} + } + return sql.NullString{ + String: h.Hex(), + Valid: true, + } +} + +func stringToNullString(s string) sql.NullString { + if s == "" { + return sql.NullString{Valid: false} + } + return sql.NullString{ + String: s, + Valid: true, + } +} + +// ToPendingProven converts a db object to a pending proven. +func (p PendingProvenModel) ToPendingProven() (*guarddb.PendingProven, error) { + txID, err := hexutil.Decode(p.TransactionID) + if err != nil { + return nil, fmt.Errorf("could not get transaction id: %w", err) + } + + transactionID, err := sliceToArray(txID) + if err != nil { + return nil, fmt.Errorf("could not convert transaction id: %w", err) + } + + return &guarddb.PendingProven{ + TransactionID: transactionID, + TxHash: common.HexToHash(p.TxHash), + Status: p.Status, + }, nil +} + +func sliceToArray(slice []byte) ([32]byte, error) { + var arr [32]byte + if len(slice) != 32 { + return arr, errors.New("slice is not 32 bytes long") + } + copy(arr[:], slice) + return arr, nil +} diff --git a/services/rfq/guard/guarddb/base/proven.go b/services/rfq/guard/guarddb/base/proven.go new file mode 100644 index 0000000000..04821910bd --- /dev/null +++ b/services/rfq/guard/guarddb/base/proven.go @@ -0,0 +1,55 @@ +package base + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// StorePendingProven stores a quote request. +func (s Store) StorePendingProven(ctx context.Context, proven guarddb.PendingProven) error { + model := FromPendingProven(proven) + dbTx := s.DB().WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: transactionIDFieldName}}, + DoUpdates: clause.AssignmentColumns([]string{transactionIDFieldName}), + }).Create(&model) + if dbTx.Error != nil { + return fmt.Errorf("could not store proven: %w", dbTx.Error) + } + return nil +} + +// GetPendingProvenByID gets a quote request by id. Should return ErrNoProvenForID if not found. +func (s Store) GetPendingProvenByID(ctx context.Context, id [32]byte) (*guarddb.PendingProven, error) { + var modelResult PendingProvenModel + tx := s.DB().WithContext(ctx).Where(fmt.Sprintf("%s = ?", transactionIDFieldName), hexutil.Encode(id[:])).First(&modelResult) + if errors.Is(tx.Error, gorm.ErrRecordNotFound) { + return nil, guarddb.ErrNoProvenForID + } + + if tx.Error != nil { + return nil, fmt.Errorf("could not get proven") + } + + qr, err := modelResult.ToPendingProven() + if err != nil { + return nil, err + } + return qr, nil +} + +// UpdatePendingProvenStatus updates the status of a pending proven. +func (s Store) UpdatePendingProvenStatus(ctx context.Context, id [32]byte, status guarddb.PendingProvenStatus) error { + tx := s.DB().WithContext(ctx).Model(&PendingProvenModel{}). + Where(fmt.Sprintf("%s = ?", transactionIDFieldName), hexutil.Encode(id[:])). + Update(statusFieldName, status) + if tx.Error != nil { + return fmt.Errorf("could not update: %w", tx.Error) + } + return nil +} diff --git a/services/rfq/guard/guarddb/base/store.go b/services/rfq/guard/guarddb/base/store.go new file mode 100644 index 0000000000..f71edc336e --- /dev/null +++ b/services/rfq/guard/guarddb/base/store.go @@ -0,0 +1,44 @@ +package base + +import ( + "github.com/synapsecns/sanguine/core/metrics" + listenerDB "github.com/synapsecns/sanguine/ethergo/listener/db" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" + "github.com/synapsecns/sanguine/ethergo/submitter/db/txdb" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "gorm.io/gorm" +) + +// Store implements the service. +type Store struct { + listenerDB.ChainListenerDB + db *gorm.DB + submitterStore submitterDB.Service +} + +// NewStore creates a new store. +func NewStore(db *gorm.DB, metrics metrics.Handler) *Store { + txDB := txdb.NewTXStore(db, metrics) + + return &Store{ChainListenerDB: listenerDB.NewChainListenerStore(db, metrics), db: db, submitterStore: txDB} +} + +// DB gets the database object for mutation outside of the lib. +func (s Store) DB() *gorm.DB { + return s.db +} + +// SubmitterDB gets the submitter database object for mutation outside of the lib. +func (s Store) SubmitterDB() submitterDB.Service { + return s.submitterStore +} + +// GetAllModels gets all models to migrate +// see: https://medium.com/@SaifAbid/slice-interfaces-8c78f8b6345d for an explanation of why we can't do this at initialization time +func GetAllModels() (allModels []interface{}) { + allModels = append(txdb.GetAllModels(), &PendingProvenModel{}) + allModels = append(allModels, listenerDB.GetAllModels()...) + return allModels +} + +var _ guarddb.Service = &Store{} diff --git a/services/rfq/guard/guarddb/connect/sql.go b/services/rfq/guard/guarddb/connect/sql.go new file mode 100644 index 0000000000..1fe39f9024 --- /dev/null +++ b/services/rfq/guard/guarddb/connect/sql.go @@ -0,0 +1,39 @@ +// Package connect contains the database connection logic for the RFQ relayer. +// TODO: this is a dumb name for a package in a dumb place. Move it somewhere else. +package connect + +import ( + "context" + "errors" + "fmt" + + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/mysql" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/sqlite" +) + +// Connect connects to the database. +func Connect(ctx context.Context, dbType dbcommon.DBType, path string, metrics metrics.Handler) (guarddb.Service, error) { + switch dbType { + case dbcommon.Mysql: + store, err := mysql.NewMysqlStore(ctx, path, metrics) + if err != nil { + return nil, fmt.Errorf("could not create mysql store: %w", err) + } + + return store, nil + case dbcommon.Sqlite: + store, err := sqlite.NewSqliteStore(ctx, path, metrics) + if err != nil { + return nil, fmt.Errorf("could not create sqlite store: %w", err) + } + + return store, nil + case dbcommon.Clickhouse: + return nil, errors.New("driver not supported") + default: + return nil, fmt.Errorf("unsupported driver: %s", dbType) + } +} diff --git a/services/rfq/guard/guarddb/db.go b/services/rfq/guard/guarddb/db.go new file mode 100644 index 0000000000..6a6b54f622 --- /dev/null +++ b/services/rfq/guard/guarddb/db.go @@ -0,0 +1,99 @@ +package guarddb + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + + "github.com/synapsecns/sanguine/ethergo/listener/db" + + "github.com/ethereum/go-ethereum/common" + "github.com/synapsecns/sanguine/core/dbcommon" + submitterDB "github.com/synapsecns/sanguine/ethergo/submitter/db" +) + +var ( + // ErrNoProvenForID means the proven was not found. + ErrNoProvenForID = errors.New("no proven found for tx id") +) + +// Writer is the interface for writing to the database. +type Writer interface { + // StorePendingProven stores a pending proven. + StorePendingProven(ctx context.Context, proven PendingProven) error + // UpdatePendingProvenStatus updates the status of a pending proven. + UpdatePendingProvenStatus(ctx context.Context, id [32]byte, status PendingProvenStatus) error +} + +// Reader is the interface for reading from the database. +type Reader interface { + // GetPendingProvenByID gets a quote request by id. Should return ErrNoProvenForID if not found + GetPendingProvenByID(ctx context.Context, id [32]byte) (*PendingProven, error) +} + +// Service is the interface for the database service. +type Service interface { + Reader + // SubmitterDB returns the submitter database service. + SubmitterDB() submitterDB.Service + Writer + db.ChainListenerDB +} + +// PendingProven is the pending proven object. +type PendingProven struct { + TransactionID [32]byte + TxHash common.Hash + Status PendingProvenStatus +} + +// PendingProvenStatus is the status of a quote request in the db. +// This is the primary mechanism for moving data through the app. +// +// TODO: consider making this an interface and exporting that. +// +// EXTREMELY IMPORTANT: DO NOT ADD NEW VALUES TO THIS ENUM UNLESS THEY ARE AT THE END. +// +//go:generate go run golang.org/x/tools/cmd/stringer -type=PendingProvenStatus +type PendingProvenStatus uint8 + +const ( + // ProveCalled means the prove() function has been called. + ProveCalled PendingProvenStatus = iota + 1 + // Validated means the prove() call has been properly validated on the dest chain. + Validated + // DisputePending means dispute() has been called in the event of an invalid prove(). + DisputePending + // Disputed means the dispute() call has been confirmed. + Disputed +) + +// Int returns the int value of the quote request status. +func (q PendingProvenStatus) Int() uint8 { + return uint8(q) +} + +// GormDataType implements the gorm common interface for enums. +func (q PendingProvenStatus) GormDataType() string { + return dbcommon.EnumDataType +} + +// Scan implements the gorm common interface for enums. +func (q *PendingProvenStatus) Scan(src any) error { + res, err := dbcommon.EnumScan(src) + if err != nil { + return fmt.Errorf("could not scan %w", err) + } + newStatus := PendingProvenStatus(res) + *q = newStatus + return nil +} + +// Value implements the gorm common interface for enums. +func (q PendingProvenStatus) Value() (driver.Value, error) { + // nolint: wrapcheck + return dbcommon.EnumValue(q) +} + +var _ dbcommon.Enum = (*PendingProvenStatus)(nil) diff --git a/services/rfq/guard/guarddb/doc.go b/services/rfq/guard/guarddb/doc.go new file mode 100644 index 0000000000..2efd9af622 --- /dev/null +++ b/services/rfq/guard/guarddb/doc.go @@ -0,0 +1,3 @@ +// Package guarddb contains the datbaase interface for the rfq guard. +// All data store types must confrm to this interface. +package guarddb diff --git a/services/rfq/guard/guarddb/mysql/mysql.go b/services/rfq/guard/guarddb/mysql/mysql.go new file mode 100644 index 0000000000..560a0a6c65 --- /dev/null +++ b/services/rfq/guard/guarddb/mysql/mysql.go @@ -0,0 +1,63 @@ +// Package mysql provides a common interface for starting sql-lite databases +package mysql + +import ( + "context" + "fmt" + "time" + + "github.com/ipfs/go-log" + "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/base" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +var logger = log.Logger("mysql-logger") + +// Store is the sqlite store. It extends the base store for sqlite specific queries. +type Store struct { + *base.Store +} + +// MaxIdleConns is exported here for testing. Tests execute too slowly with a reconnect each time. +var MaxIdleConns = 0 + +// NamingStrategy is used to exported here for testing. +var NamingStrategy = schema.NamingStrategy{} + +// NewMysqlStore creates a new mysql store for a given data store. +func NewMysqlStore(ctx context.Context, dbURL string, handler metrics.Handler) (*Store, error) { + logger.Debug("create mysql store") + + gdb, err := gorm.Open(mysql.Open(dbURL), &gorm.Config{ + Logger: dbcommon.GetGormLogger(logger), + FullSaveAssociations: true, + NamingStrategy: NamingStrategy, + NowFunc: time.Now, + }) + + if err != nil { + return nil, fmt.Errorf("could not create mysql connection: %w", err) + } + + sqlDB, err := gdb.DB() + if err != nil { + return nil, fmt.Errorf("could not get sql db: %w", err) + } + + // fixes a timeout issue https://stackoverflow.com/a/42146536 + sqlDB.SetMaxIdleConns(MaxIdleConns) + sqlDB.SetConnMaxLifetime(time.Hour) + + handler.AddGormCallbacks(gdb) + + err = gdb.WithContext(ctx).AutoMigrate(base.GetAllModels()...) + if err != nil { + return nil, fmt.Errorf("could not migrate on mysql: %w", err) + } + + return &Store{base.NewStore(gdb, handler)}, nil +} diff --git a/services/rfq/guard/guarddb/pendingprovenstatus_string.go b/services/rfq/guard/guarddb/pendingprovenstatus_string.go new file mode 100644 index 0000000000..31aa25e8e9 --- /dev/null +++ b/services/rfq/guard/guarddb/pendingprovenstatus_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type=PendingProvenStatus"; DO NOT EDIT. + +package guarddb + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProveCalled-1] + _ = x[Validated-2] + _ = x[DisputePending-3] + _ = x[Disputed-4] +} + +const _PendingProvenStatus_name = "ProveCalledValidatedDisputePendingDisputed" + +var _PendingProvenStatus_index = [...]uint8{0, 11, 20, 34, 42} + +func (i PendingProvenStatus) String() string { + i -= 1 + if i >= PendingProvenStatus(len(_PendingProvenStatus_index)-1) { + return "PendingProvenStatus(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _PendingProvenStatus_name[_PendingProvenStatus_index[i]:_PendingProvenStatus_index[i+1]] +} diff --git a/services/rfq/guard/guarddb/sqlite/sqlite.go b/services/rfq/guard/guarddb/sqlite/sqlite.go new file mode 100644 index 0000000000..0a1d4c939e --- /dev/null +++ b/services/rfq/guard/guarddb/sqlite/sqlite.go @@ -0,0 +1,62 @@ +// Package sqlite provides a common interface for starting sql-lite databases +package sqlite + +import ( + "context" + "fmt" + "os" + + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/base" + + "github.com/ipfs/go-log" + common_base "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/core/metrics" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// Store is the sqlite store. It extends the base store for sqlite specific queries. +type Store struct { + *base.Store +} + +var logger = log.Logger("rfq-sqlite") + +// NewSqliteStore creates a new sqlite data store. +func NewSqliteStore(parentCtx context.Context, dbPath string, handler metrics.Handler) (_ *Store, err error) { + logger.Debugf("creating sqlite store at %s", dbPath) + + ctx, span := handler.Tracer().Start(parentCtx, "start-sqlite") + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + // create the directory to the store if it doesn't exist + err = os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return nil, fmt.Errorf("could not create sqlite store") + } + + logger.Warnf("rfq database is at %s/synapse.db", dbPath) + + gdb, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s/%s", dbPath, "synapse.db")), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + Logger: common_base.GetGormLogger(logger), + FullSaveAssociations: true, + SkipDefaultTransaction: true, + }) + if err != nil { + return nil, fmt.Errorf("could not connect to db %s: %w", dbPath, err) + } + + handler.AddGormCallbacks(gdb) + + err = gdb.WithContext(ctx).AutoMigrate(base.GetAllModels()...) + if err != nil { + return nil, fmt.Errorf("could not migrate models: %w", err) + } + return &Store{base.NewStore(gdb, handler)}, nil +} + +var _ guarddb.Service = &Store{} From 43d10b7ec85ed9e10cd793d1a331d773f1d98943 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 16:08:34 -0500 Subject: [PATCH 03/37] WIP: db loop --- services/rfq/guard/guarddb/base/proven.go | 47 ++++++++++++---- services/rfq/guard/guarddb/db.go | 2 + services/rfq/guard/service/guard.go | 66 +++++++++++++++++++++-- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/services/rfq/guard/guarddb/base/proven.go b/services/rfq/guard/guarddb/base/proven.go index 04821910bd..c44554bbf3 100644 --- a/services/rfq/guard/guarddb/base/proven.go +++ b/services/rfq/guard/guarddb/base/proven.go @@ -24,6 +24,42 @@ func (s Store) StorePendingProven(ctx context.Context, proven guarddb.PendingPro return nil } +// UpdatePendingProvenStatus updates the status of a pending proven. +func (s Store) UpdatePendingProvenStatus(ctx context.Context, id [32]byte, status guarddb.PendingProvenStatus) error { + tx := s.DB().WithContext(ctx).Model(&PendingProvenModel{}). + Where(fmt.Sprintf("%s = ?", transactionIDFieldName), hexutil.Encode(id[:])). + Update(statusFieldName, status) + if tx.Error != nil { + return fmt.Errorf("could not update: %w", tx.Error) + } + return nil +} + +// GetPendingProvensByStatus gets pending provens by status. +func (s Store) GetPendingProvensByStatus(ctx context.Context, matchStatuses ...guarddb.PendingProvenStatus) (res []guarddb.PendingProven, _ error) { + var provenResults []PendingProvenModel + + inArgs := make([]int, len(matchStatuses)) + for i := range matchStatuses { + inArgs[i] = int(matchStatuses[i].Int()) + } + + // TODO: consider pagination + tx := s.DB().WithContext(ctx).Model(&PendingProvenModel{}).Where(fmt.Sprintf("%s IN ?", statusFieldName), inArgs).Find(&provenResults) + if tx.Error != nil { + return []guarddb.PendingProven{}, fmt.Errorf("could not get db results: %w", tx.Error) + } + + for _, result := range provenResults { + marshaled, err := result.ToPendingProven() + if err != nil { + return []guarddb.PendingProven{}, fmt.Errorf("could not get provens") + } + res = append(res, *marshaled) + } + return res, nil +} + // GetPendingProvenByID gets a quote request by id. Should return ErrNoProvenForID if not found. func (s Store) GetPendingProvenByID(ctx context.Context, id [32]byte) (*guarddb.PendingProven, error) { var modelResult PendingProvenModel @@ -42,14 +78,3 @@ func (s Store) GetPendingProvenByID(ctx context.Context, id [32]byte) (*guarddb. } return qr, nil } - -// UpdatePendingProvenStatus updates the status of a pending proven. -func (s Store) UpdatePendingProvenStatus(ctx context.Context, id [32]byte, status guarddb.PendingProvenStatus) error { - tx := s.DB().WithContext(ctx).Model(&PendingProvenModel{}). - Where(fmt.Sprintf("%s = ?", transactionIDFieldName), hexutil.Encode(id[:])). - Update(statusFieldName, status) - if tx.Error != nil { - return fmt.Errorf("could not update: %w", tx.Error) - } - return nil -} diff --git a/services/rfq/guard/guarddb/db.go b/services/rfq/guard/guarddb/db.go index 6a6b54f622..3f1db7d1e8 100644 --- a/services/rfq/guard/guarddb/db.go +++ b/services/rfq/guard/guarddb/db.go @@ -28,6 +28,8 @@ type Writer interface { // Reader is the interface for reading from the database. type Reader interface { + // GetPendingProvensByStatus gets pending provens by status. + GetPendingProvensByStatus(ctx context.Context, matchStatuses ...PendingProvenStatus) ([]*PendingProven, error) // GetPendingProvenByID gets a quote request by id. Should return ErrNoProvenForID if not found GetPendingProvenByID(ctx context.Context, id [32]byte) (*PendingProven, error) } diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index aaaaa7747a..88b9309539 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -3,6 +3,7 @@ package guard import ( "context" "fmt" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -13,10 +14,12 @@ import ( "github.com/synapsecns/sanguine/ethergo/listener" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" - "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" ) var logger = log.Logger("guard") @@ -25,6 +28,7 @@ var logger = log.Logger("guard") type Guard struct { cfg relconfig.Config metrics metrics.Handler + db guarddb.Service chainListeners map[int]listener.ContractListener contracts map[int]*fastbridge.FastBridgeRef } @@ -82,20 +86,71 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg relconfig. return &Guard{ cfg: cfg, + db: store, chainListeners: chainListeners, contracts: contracts, }, nil } +const defaultDBInterval = 5 + // Start starts the guard. -func (g *Guard) Start(ctx context.Context) error { +func (g *Guard) Start(ctx context.Context) (err error) { + group, ctx := errgroup.WithContext(ctx) + group.Go(func() error { + err := g.startChainIndexers(ctx) + if err != nil { + return fmt.Errorf("could not start chain indexers: %w", err) + } + return nil + }) + group.Go(func() error { + err = g.runDBSelector(ctx) + if err != nil { + return fmt.Errorf("could not start db selector: %w", err) + } + return nil + }) + + err = group.Wait() + if err != nil { + return fmt.Errorf("could not wait for group: %w", err) + } + + return nil +} + +func (g *Guard) runDBSelector(ctx context.Context) (err error) { + interval := g.cfg.GetDBSelectorInterval() + for { select { case <-ctx.Done(): - return nil + return fmt.Errorf("could not run db selector: %w", ctx.Err()) + case <-time.After(interval): + err := g.processDB(ctx) + if err != nil { + return err + } } } +} + +func (g *Guard) startChainIndexers(ctx context.Context) error { + group, ctx := errgroup.WithContext(ctx) + for chainID := range g.cfg.GetChains() { + chainID := chainID // capture func literal + + group.Go(func() error { + err := g.runChainIndexer(ctx, chainID) + if err != nil { + return fmt.Errorf("could not runChainIndexer chain indexer for chain %d: %w", chainID, err) + } + return nil + }) + } + return nil } func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { @@ -152,3 +207,8 @@ func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.Fa // } return nil } + +func (g *Guard) processDB(ctx context.Context) (err error) { + provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.PendingProvenStatusPending) + +} From 49e9466ea3aba9df5405d7ddb7f8e40ba87f112f Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 16:25:48 -0500 Subject: [PATCH 04/37] Feat: add BridgeRequest model --- services/rfq/guard/guarddb/base/bridge.go | 44 ++++++++++ services/rfq/guard/guarddb/base/model.go | 100 ++++++++++++++++++++++ services/rfq/guard/guarddb/db.go | 16 +++- services/rfq/guard/service/guard.go | 17 +++- 4 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 services/rfq/guard/guarddb/base/bridge.go diff --git a/services/rfq/guard/guarddb/base/bridge.go b/services/rfq/guard/guarddb/base/bridge.go new file mode 100644 index 0000000000..965d49eced --- /dev/null +++ b/services/rfq/guard/guarddb/base/bridge.go @@ -0,0 +1,44 @@ +package base + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// StoreBridgeRequest stores a quote request. +func (s Store) StoreBridgeRequest(ctx context.Context, request guarddb.BridgeRequest) error { + model := FromBridgeRequest(request) + dbTx := s.DB().WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: transactionIDFieldName}}, + DoUpdates: clause.AssignmentColumns([]string{transactionIDFieldName}), + }).Create(&model) + if dbTx.Error != nil { + return fmt.Errorf("could not store request: %w", dbTx.Error) + } + return nil +} + +// GetBridgeRequestByID gets a quote request by id. Should return ErrNoBridgeRequestForID if not found. +func (s Store) GetBridgeRequestByID(ctx context.Context, id [32]byte) (*guarddb.BridgeRequest, error) { + var modelResult BridgeRequestModel + tx := s.DB().WithContext(ctx).Where(fmt.Sprintf("%s = ?", transactionIDFieldName), hexutil.Encode(id[:])).First(&modelResult) + if errors.Is(tx.Error, gorm.ErrRecordNotFound) { + return nil, guarddb.ErrNoBridgeRequestForID + } + + if tx.Error != nil { + return nil, fmt.Errorf("could not get request") + } + + qr, err := modelResult.ToBridgeRequest() + if err != nil { + return nil, err + } + return qr, nil +} diff --git a/services/rfq/guard/guarddb/base/model.go b/services/rfq/guard/guarddb/base/model.go index 26a9cbbeac..306071d3d9 100644 --- a/services/rfq/guard/guarddb/base/model.go +++ b/services/rfq/guard/guarddb/base/model.go @@ -4,11 +4,13 @@ import ( "database/sql" "errors" "fmt" + "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/synapsecns/sanguine/core/dbcommon" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" ) @@ -101,6 +103,104 @@ func (p PendingProvenModel) ToPendingProven() (*guarddb.PendingProven, error) { }, nil } +// BridgeRequestModel is the primary event model. +type BridgeRequestModel struct { + // CreatedAt is the creation time + CreatedAt time.Time + // UpdatedAt is the update time + UpdatedAt time.Time + // TransactionID is the transaction id of the event + TransactionID string `gorm:"column:transaction_id;primaryKey"` + // OriginChainID is the origin chain for the transactions + OriginChainID uint32 + // DestChainID is the destination chain for the tx + DestChainID uint32 + // OriginSender is the original sender + OriginSender string + // DestRecipient is the recipient of the destination tx + DestRecipient string + // OriginToken is the origin token address + OriginToken string + // DestToken is the destination token address + DestToken string + // OriginAmount is the origin amount stored for sorting. + // This is not the source of truth, but is approximate + OriginAmount string + // DestAmount is the destination amount stored for sorting. + DestAmount string + // Deadline is the deadline for the relay + Deadline time.Time `gorm:"index"` + // OriginNonce is the nonce on the origin chain in the app. + // this is not effected by the message.sender nonce. + OriginNonce int `gorm:"index"` + // RawRequest is the raw request, hex encoded. + RawRequest string + // SendChainGas is true if the chain should send gas + SendChainGas bool +} + +func FromBridgeRequest(request guarddb.BridgeRequest) BridgeRequestModel { + return BridgeRequestModel{ + TransactionID: hexutil.Encode(request.TransactionID[:]), + OriginChainID: request.Transaction.OriginChainId, + DestChainID: request.Transaction.DestChainId, + OriginSender: request.Transaction.OriginSender.String(), + DestRecipient: request.Transaction.DestRecipient.String(), + OriginToken: request.Transaction.OriginToken.String(), + RawRequest: hexutil.Encode(request.RawRequest), + SendChainGas: request.Transaction.SendChainGas, + DestToken: request.Transaction.DestToken.String(), + OriginAmount: request.Transaction.OriginAmount.String(), + DestAmount: request.Transaction.DestAmount.String(), + Deadline: time.Unix(int64(request.Transaction.Deadline.Uint64()), 0), + OriginNonce: int(request.Transaction.Nonce.Uint64()), + } +} + +func (b BridgeRequestModel) ToBridgeRequest() (*guarddb.BridgeRequest, error) { + txID, err := hexutil.Decode(b.TransactionID) + if err != nil { + return nil, fmt.Errorf("could not get transaction id: %w", err) + } + + req, err := hexutil.Decode(b.RawRequest) + if err != nil { + return nil, fmt.Errorf("could not get request: %w", err) + } + + transactionID, err := sliceToArray(txID) + if err != nil { + return nil, fmt.Errorf("could not convert transaction id: %w", err) + } + + originAmount, ok := new(big.Int).SetString(b.OriginAmount, 10) + if !ok { + return nil, errors.New("could not convert origin amount") + } + destAmount, ok := new(big.Int).SetString(b.DestAmount, 10) + if !ok { + return nil, errors.New("could not convert dest amount") + } + + return &guarddb.BridgeRequest{ + TransactionID: transactionID, + RawRequest: req, + Transaction: fastbridge.IFastBridgeBridgeTransaction{ + OriginChainId: b.OriginChainID, + DestChainId: b.DestChainID, + OriginSender: common.HexToAddress(b.OriginSender), + DestRecipient: common.HexToAddress(b.DestRecipient), + OriginToken: common.HexToAddress(b.OriginToken), + SendChainGas: b.SendChainGas, + DestToken: common.HexToAddress(b.DestToken), + OriginAmount: originAmount, + DestAmount: destAmount, + Deadline: big.NewInt(b.Deadline.Unix()), + Nonce: big.NewInt(int64(b.OriginNonce)), + }, + }, nil +} + func sliceToArray(slice []byte) ([32]byte, error) { var arr [32]byte if len(slice) != 32 { diff --git a/services/rfq/guard/guarddb/db.go b/services/rfq/guard/guarddb/db.go index 3f1db7d1e8..3151b3e4fa 100644 --- a/services/rfq/guard/guarddb/db.go +++ b/services/rfq/guard/guarddb/db.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/synapsecns/sanguine/ethergo/listener/db" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/ethereum/go-ethereum/common" "github.com/synapsecns/sanguine/core/dbcommon" @@ -16,10 +17,14 @@ import ( var ( // ErrNoProvenForID means the proven was not found. ErrNoProvenForID = errors.New("no proven found for tx id") + // ErrNoBridgeRequestForID means the bridge request was not found. + ErrNoBridgeRequestForID = errors.New("no bridge request found for tx id") ) // Writer is the interface for writing to the database. type Writer interface { + // StoreBridgeRequest stores a bridge request. + StoreBridgeRequest(ctx context.Context, request BridgeRequest) error // StorePendingProven stores a pending proven. StorePendingProven(ctx context.Context, proven PendingProven) error // UpdatePendingProvenStatus updates the status of a pending proven. @@ -30,8 +35,10 @@ type Writer interface { type Reader interface { // GetPendingProvensByStatus gets pending provens by status. GetPendingProvensByStatus(ctx context.Context, matchStatuses ...PendingProvenStatus) ([]*PendingProven, error) - // GetPendingProvenByID gets a quote request by id. Should return ErrNoProvenForID if not found + // GetPendingProvenByID gets a pending proven by id. Should return ErrNoProvenForID if not found GetPendingProvenByID(ctx context.Context, id [32]byte) (*PendingProven, error) + // GetBridgeRequestByID gets a bridge request by id. Should return ErrNoBridgeRequestForID if not found + GetBridgeRequestByID(ctx context.Context, id [32]byte) (*BridgeRequest, error) } // Service is the interface for the database service. @@ -43,6 +50,13 @@ type Service interface { db.ChainListenerDB } +// BridgeRequest is the bridge request object. +type BridgeRequest struct { + TransactionID [32]byte + Transaction fastbridge.IFastBridgeBridgeTransaction + RawRequest []byte +} + // PendingProven is the pending proven object. type PendingProven struct { TransactionID [32]byte diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 88b9309539..c4afbca949 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -209,6 +209,21 @@ func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.Fa } func (g *Guard) processDB(ctx context.Context) (err error) { - provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.PendingProvenStatusPending) + provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.ProveCalled) + for _, proven := range provens { + err := g.handleProveCalled(ctx, proven) + if err != nil { + return fmt.Errorf("could not handle prove called: %w", err) + } + } + return nil +} + +func (g *Guard) handleProveCalled(proven guarddb.PendingProven) (err error) { + // contract, ok := g.contracts[proven.Origin] + // if !ok { + // return fmt.Errorf("could not get contract for chain: %d", proven.Origin) + // } + return nil } From 85cbf82b34d5f33dd157ca2f977c9c818263d438 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 16:29:45 -0500 Subject: [PATCH 05/37] Feat: store bridge request --- services/rfq/guard/service/guard.go | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index c4afbca949..1744d4e2ec 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -11,6 +11,7 @@ import ( "github.com/ipfs/go-log" "github.com/synapsecns/sanguine/core/dbcommon" "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/ethergo/listener" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" @@ -29,6 +30,7 @@ type Guard struct { cfg relconfig.Config metrics metrics.Handler db guarddb.Service + client omniClient.RPCClient chainListeners map[int]listener.ContractListener contracts map[int]*fastbridge.FastBridgeRef } @@ -87,6 +89,7 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg relconfig. return &Guard{ cfg: cfg, db: store, + client: omniClient, chainListeners: chainListeners, contracts: contracts, }, nil @@ -184,6 +187,11 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { }() switch event := parsedEvent.(type) { + case *fastbridge.FastBridgeBridgeRequested: + err = g.handleBridgeRequestedLog(ctx, event, uint64(chainID)) + if err != nil { + return fmt.Errorf("could not handle request: %w", err) + } case *fastbridge.FastBridgeBridgeProofProvided: err = g.handleProofProvidedLog(ctx, event, chainID) if err != nil { @@ -200,6 +208,44 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { return nil } +var maxRPCRetryTime = 15 * time.Second + +func (g *Guard) handleBridgeRequestedLog(ctx context.Context, req *fastbridge.FastBridgeBridgeRequested, chainID int) (err error) { + originClient, err := g.client.GetChainClient(ctx, int(chainID)) + if err != nil { + return fmt.Errorf("could not get correct omnirpc client: %w", err) + } + + fastBridge, err := fastbridge.NewFastBridgeRef(req.Raw.Address, originClient) + if err != nil { + return fmt.Errorf("could not get correct fast bridge: %w", err) + } + + var bridgeTx fastbridge.IFastBridgeBridgeTransaction + call := func(ctx context.Context) error { + bridgeTx, err = fastBridge.GetBridgeTransaction(&bind.CallOpts{Context: ctx}, req.Request) + if err != nil { + return fmt.Errorf("could not get bridge transaction: %w", err) + } + return nil + } + err = retry.WithBackoff(ctx, call, retry.WithMaxTotalTime(maxRPCRetryTime)) + if err != nil { + return fmt.Errorf("could not make call: %w", err) + } + + dbReq := guarddb.BridgeRequest{ + RawRequest: req.Request, + TransactionID: req.TransactionId, + Transaction: bridgeTx, + } + err = g.db.StoreBridgeRequest(ctx, dbReq) + if err != nil { + return fmt.Errorf("could not get db: %w", err) + } + return nil +} + func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { // contract, ok := g.contracts[chainID] // if !ok { From 35bf205aee43e28fcc17fd4472fd4782513014d5 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 16:56:07 -0500 Subject: [PATCH 06/37] Feat: add dispute trigger --- services/rfq/guard/guarddb/base/model.go | 4 + services/rfq/guard/guarddb/db.go | 1 + services/rfq/guard/service/guard.go | 107 ++++++++++++++++++++--- 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/services/rfq/guard/guarddb/base/model.go b/services/rfq/guard/guarddb/base/model.go index 306071d3d9..3171802ba8 100644 --- a/services/rfq/guard/guarddb/base/model.go +++ b/services/rfq/guard/guarddb/base/model.go @@ -45,6 +45,8 @@ type PendingProvenModel struct { CreatedAt time.Time // UpdatedAt is the update time UpdatedAt time.Time + // Origin is the origin chain id + Origin uint32 // TransactionID is the transaction id of the event TransactionID string `gorm:"column:transaction_id;primaryKey"` // TxHash is the hash of the relay transaction on destination @@ -56,6 +58,7 @@ type PendingProvenModel struct { // FromPendingProven converts a quote request to an object that can be stored in the db. func FromPendingProven(proven guarddb.PendingProven) PendingProvenModel { return PendingProvenModel{ + Origin: proven.Origin, TransactionID: hexutil.Encode(proven.TransactionID[:]), TxHash: proven.TxHash.Hex(), Status: proven.Status, @@ -97,6 +100,7 @@ func (p PendingProvenModel) ToPendingProven() (*guarddb.PendingProven, error) { } return &guarddb.PendingProven{ + Origin: p.Origin, TransactionID: transactionID, TxHash: common.HexToHash(p.TxHash), Status: p.Status, diff --git a/services/rfq/guard/guarddb/db.go b/services/rfq/guard/guarddb/db.go index 3151b3e4fa..236a5b7e2c 100644 --- a/services/rfq/guard/guarddb/db.go +++ b/services/rfq/guard/guarddb/db.go @@ -59,6 +59,7 @@ type BridgeRequest struct { // PendingProven is the pending proven object. type PendingProven struct { + Origin uint32 TransactionID [32]byte TxHash common.Hash Status PendingProvenStatus diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 1744d4e2ec..59e405194d 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -3,6 +3,7 @@ package guard import ( "context" "fmt" + "math/big" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -13,6 +14,7 @@ import ( "github.com/synapsecns/sanguine/core/metrics" "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/ethergo/listener" + "github.com/synapsecns/sanguine/ethergo/submitter" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" @@ -33,6 +35,7 @@ type Guard struct { client omniClient.RPCClient chainListeners map[int]listener.ContractListener contracts map[int]*fastbridge.FastBridgeRef + txSubmitter submitter.TransactionSubmitter } // NewGuard creates a new Guard. @@ -188,7 +191,7 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { switch event := parsedEvent.(type) { case *fastbridge.FastBridgeBridgeRequested: - err = g.handleBridgeRequestedLog(ctx, event, uint64(chainID)) + err = g.handleBridgeRequestedLog(ctx, event, chainID) if err != nil { return fmt.Errorf("could not handle request: %w", err) } @@ -247,10 +250,17 @@ func (g *Guard) handleBridgeRequestedLog(ctx context.Context, req *fastbridge.Fa } func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { - // contract, ok := g.contracts[chainID] - // if !ok { - // return fmt.Errorf("could not get contract for chain: %d", chainID) - // } + proven := guarddb.PendingProven{ + Origin: uint32(chainID), + TransactionID: event.TransactionId, + TxHash: event.TransactionHash, + Status: guarddb.ProveCalled, + } + err = g.db.StorePendingProven(ctx, proven) + if err != nil { + return fmt.Errorf("could not store pending proven: %w", err) + } + return nil } @@ -266,10 +276,87 @@ func (g *Guard) processDB(ctx context.Context) (err error) { return nil } -func (g *Guard) handleProveCalled(proven guarddb.PendingProven) (err error) { - // contract, ok := g.contracts[proven.Origin] - // if !ok { - // return fmt.Errorf("could not get contract for chain: %d", proven.Origin) - // } +func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingProven) (err error) { + // first, get the corresponding bridge request + bridgeRequest, err := g.db.GetBridgeRequestByID(ctx, proven.TransactionID) + if err != nil { + return fmt.Errorf("could not get bridge request: %w", err) + } + + valid, err := g.isProveValid(ctx, proven, bridgeRequest) + if err != nil { + return fmt.Errorf("could not check prove validity: %w", err) + } + + if valid { + // mark as validated + err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Validated) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + } else { + // trigger dispute + contract, ok := g.contracts[int(bridgeRequest.Transaction.DestChainId)] + if !ok { + return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.DestChainId) + } + _, err := g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(proven.Origin)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + tx, err = contract.Dispute(&bind.TransactOpts{Context: ctx}, proven.TransactionID) + if err != nil { + return nil, fmt.Errorf("could not dispute: %w", err) + } + + return tx, nil + }) + if err != nil { + return fmt.Errorf("could not dispute: %w", err) + } + + // mark as disputed + err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Disputed) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + } + return nil } + +func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, bridgeRequest *guarddb.BridgeRequest) (bool, error) { + // get the receipt for this tx on dest chain + chainClient, err := g.client.GetChainClient(ctx, int(bridgeRequest.Transaction.DestChainId)) + if err != nil { + return false, fmt.Errorf("could not get chain client: %w", err) + } + receipt, err := chainClient.TransactionReceipt(ctx, proven.TxHash) + if err != nil { + return false, fmt.Errorf("could not get receipt: %w", err) + } + addr, err := g.cfg.GetRFQAddress(int(bridgeRequest.Transaction.DestChainId)) + if err != nil { + return false, fmt.Errorf("could not get rfq address: %w", err) + } + parser, err := fastbridge.NewParser(common.HexToAddress(addr)) + if err != nil { + return false, fmt.Errorf("could not get parser: %w", err) + } + + for _, log := range receipt.Logs { + _, parsedEvent, ok := parser.ParseEvent(*log) + if !ok { + continue + } + + switch event := parsedEvent.(type) { + case *fastbridge.FastBridgeBridgeRelayed: + return relayMatchesBridgeRequest(event, bridgeRequest), nil + } + } + + return false, nil +} + +func relayMatchesBridgeRequest(event *fastbridge.FastBridgeBridgeRelayed, bridgeRequest *guarddb.BridgeRequest) bool { + //TODO: implement + return true +} From ebb8847a6433193eef1c629bb5693eb7928616bb Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 16:59:23 -0500 Subject: [PATCH 07/37] Feat: handle disputed log --- services/rfq/guard/service/guard.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 59e405194d..fe9d1128f4 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -200,6 +200,11 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { if err != nil { return fmt.Errorf("could not handle request: %w", err) } + case *fastbridge.FastBridgeBridgeProofDisputed: + err = g.handleProofDisputedLog(ctx, event) + if err != nil { + return fmt.Errorf("could not handle request: %w", err) + } } return nil @@ -264,6 +269,15 @@ func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.Fa return nil } +func (g *Guard) handleProofDisputedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofDisputed) (err error) { + err = g.db.UpdatePendingProvenStatus(ctx, event.TransactionId, guarddb.Disputed) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + + return nil +} + func (g *Guard) processDB(ctx context.Context) (err error) { provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.ProveCalled) for _, proven := range provens { @@ -312,8 +326,8 @@ func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingPr return fmt.Errorf("could not dispute: %w", err) } - // mark as disputed - err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Disputed) + // mark as dispute pending + err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.DisputePending) if err != nil { return fmt.Errorf("could not update pending proven status: %w", err) } From 1aea0429a1bcd9ab4df089e1994f981f171112b0 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Tue, 2 Jul 2024 17:03:56 -0500 Subject: [PATCH 08/37] Feat: implement relayMatchesBridgeRequest() --- services/rfq/guard/service/guard.go | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index fe9d1128f4..721cd4603c 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -14,6 +14,7 @@ import ( "github.com/synapsecns/sanguine/core/metrics" "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/ethergo/listener" + signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" "github.com/synapsecns/sanguine/ethergo/submitter" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" @@ -89,12 +90,22 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg relconfig. } } + sg, err := signerConfig.SignerFromConfig(ctx, cfg.Signer) + if err != nil { + return nil, fmt.Errorf("could not get signer: %w", err) + } + fmt.Printf("loaded signer with address: %s\n", sg.Address().String()) + + txSubmitter := submitter.NewTransactionSubmitter(metricHandler, sg, omniClient, store.SubmitterDB(), &cfg.SubmitterConfig) + return &Guard{ cfg: cfg, + metrics: metricHandler, db: store, client: omniClient, chainListeners: chainListeners, contracts: contracts, + txSubmitter: txSubmitter, }, nil } @@ -371,6 +382,27 @@ func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, } func relayMatchesBridgeRequest(event *fastbridge.FastBridgeBridgeRelayed, bridgeRequest *guarddb.BridgeRequest) bool { - //TODO: implement + //TODO: is this exhaustive? + if event.TransactionId != bridgeRequest.TransactionID { + return false + } + if event.OriginAmount.Cmp(bridgeRequest.Transaction.OriginAmount) != 0 { + return false + } + if event.DestAmount.Cmp(bridgeRequest.Transaction.DestAmount) != 0 { + return false + } + if event.OriginChainId != bridgeRequest.Transaction.OriginChainId { + return false + } + if event.To != bridgeRequest.Transaction.DestRecipient { + return false + } + if event.OriginToken != bridgeRequest.Transaction.OriginToken { + return false + } + if event.DestToken != bridgeRequest.Transaction.DestToken { + return false + } return true } From 9c54eee38668ca1a9ab144f4e4189b39d3ecfa1c Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 11:31:25 -0500 Subject: [PATCH 09/37] Fix: build --- services/rfq/guard/guarddb/base/model.go | 12 ------------ services/rfq/guard/guarddb/base/proven.go | 8 ++++---- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/services/rfq/guard/guarddb/base/model.go b/services/rfq/guard/guarddb/base/model.go index 3171802ba8..1e2b48d059 100644 --- a/services/rfq/guard/guarddb/base/model.go +++ b/services/rfq/guard/guarddb/base/model.go @@ -18,10 +18,6 @@ func init() { namer := dbcommon.NewNamer(GetAllModels()) statusFieldName = namer.GetConsistentName("Status") transactionIDFieldName = namer.GetConsistentName("TransactionID") - originTxHashFieldName = namer.GetConsistentName("OriginTxHash") - destTxHashFieldName = namer.GetConsistentName("DestTxHash") - rebalanceIDFieldName = namer.GetConsistentName("RebalanceID") - relayNonceFieldName = namer.GetConsistentName("RelayNonce") } var ( @@ -29,14 +25,6 @@ var ( statusFieldName string // transactionIDFieldName is the transactions id field name. transactionIDFieldName string - // originTxHashFieldName is the origin tx hash field name. - originTxHashFieldName string - // destTxHashFieldName is the dest tx hash field name. - destTxHashFieldName string - // rebalanceIDFieldName is the rebalances id field name. - rebalanceIDFieldName string - // relayNonceFieldName is the relay nonce field name. - relayNonceFieldName string ) // PendingProvenModel is the primary event model. diff --git a/services/rfq/guard/guarddb/base/proven.go b/services/rfq/guard/guarddb/base/proven.go index c44554bbf3..32da3915f6 100644 --- a/services/rfq/guard/guarddb/base/proven.go +++ b/services/rfq/guard/guarddb/base/proven.go @@ -36,7 +36,7 @@ func (s Store) UpdatePendingProvenStatus(ctx context.Context, id [32]byte, statu } // GetPendingProvensByStatus gets pending provens by status. -func (s Store) GetPendingProvensByStatus(ctx context.Context, matchStatuses ...guarddb.PendingProvenStatus) (res []guarddb.PendingProven, _ error) { +func (s Store) GetPendingProvensByStatus(ctx context.Context, matchStatuses ...guarddb.PendingProvenStatus) (res []*guarddb.PendingProven, _ error) { var provenResults []PendingProvenModel inArgs := make([]int, len(matchStatuses)) @@ -47,15 +47,15 @@ func (s Store) GetPendingProvensByStatus(ctx context.Context, matchStatuses ...g // TODO: consider pagination tx := s.DB().WithContext(ctx).Model(&PendingProvenModel{}).Where(fmt.Sprintf("%s IN ?", statusFieldName), inArgs).Find(&provenResults) if tx.Error != nil { - return []guarddb.PendingProven{}, fmt.Errorf("could not get db results: %w", tx.Error) + return []*guarddb.PendingProven{}, fmt.Errorf("could not get db results: %w", tx.Error) } for _, result := range provenResults { marshaled, err := result.ToPendingProven() if err != nil { - return []guarddb.PendingProven{}, fmt.Errorf("could not get provens") + return []*guarddb.PendingProven{}, fmt.Errorf("could not get provens") } - res = append(res, *marshaled) + res = append(res, marshaled) } return res, nil } From bdbaa7ebe112c3a1ef9d90685dc8fe29b5bdab39 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 11:40:52 -0500 Subject: [PATCH 10/37] Fix: guarddb models --- services/rfq/guard/guarddb/base/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/guard/guarddb/base/store.go b/services/rfq/guard/guarddb/base/store.go index f71edc336e..b4e08a9bae 100644 --- a/services/rfq/guard/guarddb/base/store.go +++ b/services/rfq/guard/guarddb/base/store.go @@ -36,7 +36,7 @@ func (s Store) SubmitterDB() submitterDB.Service { // GetAllModels gets all models to migrate // see: https://medium.com/@SaifAbid/slice-interfaces-8c78f8b6345d for an explanation of why we can't do this at initialization time func GetAllModels() (allModels []interface{}) { - allModels = append(txdb.GetAllModels(), &PendingProvenModel{}) + allModels = append(txdb.GetAllModels(), &PendingProvenModel{}, &BridgeRequestModel{}) allModels = append(allModels, listenerDB.GetAllModels()...) return allModels } From 7864840a243078461e147b5b3b69d0eac15d5ebf Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 11:41:03 -0500 Subject: [PATCH 11/37] Feat: check verified status in e2e test --- services/rfq/e2e/rfq_test.go | 12 ++++++ services/rfq/e2e/setup_test.go | 71 ++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index e8a76a29df..660cb3d8eb 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -19,6 +19,8 @@ import ( omnirpcClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/api/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + guardService "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb" "github.com/synapsecns/sanguine/services/rfq/relayer/service" @@ -36,8 +38,10 @@ type IntegrationSuite struct { omniClient omnirpcClient.RPCClient metrics metrics.Handler store reldb.Service + guardStore guarddb.Service apiServer string relayer *service.Relayer + guard *guardService.Guard relayerWallet wallet.Wallet userWallet wallet.Wallet } @@ -82,6 +86,7 @@ func (i *IntegrationSuite) SetupTest() { // setup the api server i.setupQuoterAPI() i.setupRelayer() + i.setupGuard() } // getOtherBackend gets the backend that is not the current one. This is a helper @@ -240,6 +245,13 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { i.NoError(err) return len(originPendingRebals) > 0 }) + + i.Eventually(func() bool { + // verify that the guard has marked the tx as validated + results, err := i.guardStore.GetPendingProvensByStatus(i.GetTestContext(), guarddb.Validated) + i.NoError(err) + return len(results) == 1 + }) } // nolint: cyclop diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index 6df213a31e..9f13cf663b 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -32,6 +32,8 @@ import ( "github.com/synapsecns/sanguine/services/rfq/api/db/sql" "github.com/synapsecns/sanguine/services/rfq/api/rest" "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20" + guardConnect "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect" + guardService "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" @@ -217,36 +219,14 @@ func (i *IntegrationSuite) Approve(backend backends.SimulatedTestBackend, token } } -func (i *IntegrationSuite) setupRelayer() { - // add myself as a filler - var wg sync.WaitGroup - wg.Add(2) - - for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { - go func(backend backends.SimulatedTestBackend) { - defer wg.Done() - - metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend) - - txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr()) - relayerRole, err := rfqContract.RELAYERROLE(&bind.CallOpts{Context: i.GetTestContext()}) - i.NoError(err) - - tx, err := rfqContract.GrantRole(txContext.TransactOpts, relayerRole, i.relayerWallet.Address()) - i.NoError(err) - - backend.WaitForConfirmation(i.GetTestContext(), tx) - }(backend) - } - wg.Wait() - +func (i *IntegrationSuite) getRelayerConfig() relconfig.Config { // construct the config relayerAPIPort, err := freeport.GetFreePort() i.NoError(err) dsn := filet.TmpDir(i.T(), "") cctpContractOrigin, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.originBackend) cctpContractDest, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.destBackend) - cfg := relconfig.Config{ + return relconfig.Config{ // generated ex-post facto Chains: map[int]relconfig.ChainConfig{ originBackendChainID: { @@ -300,6 +280,32 @@ func (i *IntegrationSuite) setupRelayer() { }, RebalanceInterval: 0, } +} + +func (i *IntegrationSuite) setupRelayer() { + // add myself as a filler + var wg sync.WaitGroup + wg.Add(2) + + for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { + go func(backend backends.SimulatedTestBackend) { + defer wg.Done() + + metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend) + + txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr()) + relayerRole, err := rfqContract.RELAYERROLE(&bind.CallOpts{Context: i.GetTestContext()}) + i.NoError(err) + + tx, err := rfqContract.GrantRole(txContext.TransactOpts, relayerRole, i.relayerWallet.Address()) + i.NoError(err) + + backend.WaitForConfirmation(i.GetTestContext(), tx) + }(backend) + } + wg.Wait() + + cfg := i.getRelayerConfig() // in the first backend, we want to deploy a bunch of different tokens // TODO: functionalize me. @@ -375,6 +381,7 @@ func (i *IntegrationSuite) setupRelayer() { } // TODO: good chance we wanna leave actually starting this up to the indiividual test. + var err error i.relayer, err = service.NewRelayer(i.GetTestContext(), i.metrics, cfg) i.NoError(err) go func() { @@ -386,3 +393,19 @@ func (i *IntegrationSuite) setupRelayer() { i.store, err = connect.Connect(i.GetTestContext(), dbType, cfg.Database.DSN, i.metrics) i.NoError(err) } + +func (i *IntegrationSuite) setupGuard() { + cfg := i.getRelayerConfig() + + var err error + i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, cfg) + i.NoError(err) + go func() { + err = i.guard.Start(i.GetTestContext()) + }() + + dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type) + i.NoError(err) + i.guardStore, err = guardConnect.Connect(i.GetTestContext(), dbType, cfg.Database.DSN, i.metrics) + i.NoError(err) +} From ff7d1ca9ac758fac3c17e8bcb23625af8bacecab Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:34:18 -0500 Subject: [PATCH 12/37] Feat: check verified status in TestETHtoETH --- services/rfq/e2e/rfq_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 660cb3d8eb..a365c9c9a5 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -359,4 +359,11 @@ func (i *IntegrationSuite) TestETHtoETH() { } return false }) + + i.Eventually(func() bool { + // verify that the guard has marked the tx as validated + results, err := i.guardStore.GetPendingProvensByStatus(i.GetTestContext(), guarddb.Validated) + i.NoError(err) + return len(results) == 1 + }) } From b2d25e99f3352d1122a6d86e41d1075b0303a1da Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:37:43 -0500 Subject: [PATCH 13/37] Feat: add guard cmd --- services/rfq/guard/cmd/cmd.go | 35 ++++++++++++++++++++++ services/rfq/guard/cmd/commands.go | 46 +++++++++++++++++++++++++++++ services/rfq/guard/service/guard.go | 2 +- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 services/rfq/guard/cmd/cmd.go create mode 100644 services/rfq/guard/cmd/commands.go diff --git a/services/rfq/guard/cmd/cmd.go b/services/rfq/guard/cmd/cmd.go new file mode 100644 index 0000000000..5a171b500f --- /dev/null +++ b/services/rfq/guard/cmd/cmd.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + + "github.com/synapsecns/sanguine/core/commandline" + "github.com/synapsecns/sanguine/core/config" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/urfave/cli/v2" +) + +// Start starts the command line tool. +func Start(args []string, buildInfo config.BuildInfo) { + app := cli.NewApp() + app.Name = buildInfo.Name() + app.Description = buildInfo.VersionString() + "Synapse RFQ Guard" + app.Usage = fmt.Sprintf("%s --help", buildInfo.Name()) + app.EnableBashCompletion = true + // TODO: should we really halt boot on because of metrics? + app.Before = func(c *cli.Context) error { + // nolint:wrapcheck + return metrics.Setup(c.Context, buildInfo) + } + + // commands + app.Commands = cli.Commands{runCommand} + shellCommand := commandline.GenerateShellCommand(app.Commands) + app.Commands = append(app.Commands, shellCommand) + app.Action = shellCommand.Action + + err := app.Run(args) + if err != nil { + panic(err) + } +} diff --git a/services/rfq/guard/cmd/commands.go b/services/rfq/guard/cmd/commands.go new file mode 100644 index 0000000000..8120d70324 --- /dev/null +++ b/services/rfq/guard/cmd/commands.go @@ -0,0 +1,46 @@ +// Package cmd provides the command line interface for the RFQ guard service +package cmd + +import ( + "fmt" + + "github.com/synapsecns/sanguine/core" + "github.com/synapsecns/sanguine/core/commandline" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/services/rfq/guard/service" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" + "github.com/urfave/cli/v2" +) + +var configFlag = &cli.StringFlag{ + Name: "config", + Usage: "path to the config file", + TakesFile: true, +} + +// runCommand runs the rfq guard. +var runCommand = &cli.Command{ + Name: "run", + Description: "run the guard", + Flags: []cli.Flag{configFlag, &commandline.LogLevel}, + Action: func(c *cli.Context) (err error) { + commandline.SetLogLevel(c) + cfg, err := relconfig.LoadConfig(core.ExpandOrReturnPath(c.String(configFlag.Name))) + if err != nil { + return fmt.Errorf("could not read config file: %w", err) + } + + metricsProvider := metrics.Get() + + guard, err := service.NewGuard(c.Context, metricsProvider, cfg) + if err != nil { + return fmt.Errorf("could not create guard: %w", err) + } + + err = guard.Start(c.Context) + if err != nil { + return fmt.Errorf("could not start guard: %w", err) + } + return nil + }, +} diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 721cd4603c..179a3bdfc6 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -1,4 +1,4 @@ -package guard +package service import ( "context" From 76b2e1a6e1fc62b6421f653b5d523fddbc1121a3 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:41:52 -0500 Subject: [PATCH 14/37] Feat: add embedded guard to relayer --- services/rfq/relayer/service/relayer.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index ace4355d54..134aa32c26 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -26,6 +26,7 @@ import ( omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" rfqAPIClient "github.com/synapsecns/sanguine/services/rfq/api/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + serviceGuard "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/inventory" "github.com/synapsecns/sanguine/services/rfq/relayer/pricer" "github.com/synapsecns/sanguine/services/rfq/relayer/quoter" @@ -326,6 +327,21 @@ func (r *Relayer) startCCTPRelayer(ctx context.Context) (err error) { return nil } +// startGuard starts the guard, if specified +func (r *Relayer) startGuard(ctx context.Context) (err error) { + guard, err := serviceGuard.NewGuard(ctx, r.metrics, r.cfg) + if err != nil { + return fmt.Errorf("could not create guard: %w", err) + } + + err = guard.Start(ctx) + if err != nil { + return fmt.Errorf("could not start guard: %w", err) + } + + return nil +} + func (r *Relayer) processDB(ctx context.Context, serial bool, matchStatuses ...reldb.QuoteRequestStatus) (err error) { ctx, span := r.metrics.Tracer().Start(ctx, "processDB", trace.WithAttributes( attribute.Bool("serial", serial), From 278cbb95fc8f156159e48cb0ab8bc1b5036d2cab Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:50:48 -0500 Subject: [PATCH 15/37] Feat: add guard config --- services/rfq/guard/cmd/commands.go | 4 +- services/rfq/guard/guardconfig/config.go | 103 +++++++++++++++++++++++ services/rfq/guard/service/guard.go | 6 +- 3 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 services/rfq/guard/guardconfig/config.go diff --git a/services/rfq/guard/cmd/commands.go b/services/rfq/guard/cmd/commands.go index 8120d70324..eab8721b22 100644 --- a/services/rfq/guard/cmd/commands.go +++ b/services/rfq/guard/cmd/commands.go @@ -7,8 +7,8 @@ import ( "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/commandline" "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig" "github.com/synapsecns/sanguine/services/rfq/guard/service" - "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/urfave/cli/v2" ) @@ -25,7 +25,7 @@ var runCommand = &cli.Command{ Flags: []cli.Flag{configFlag, &commandline.LogLevel}, Action: func(c *cli.Context) (err error) { commandline.SetLogLevel(c) - cfg, err := relconfig.LoadConfig(core.ExpandOrReturnPath(c.String(configFlag.Name))) + cfg, err := guardconfig.LoadConfig(core.ExpandOrReturnPath(c.String(configFlag.Name))) if err != nil { return fmt.Errorf("could not read config file: %w", err) } diff --git a/services/rfq/guard/guardconfig/config.go b/services/rfq/guard/guardconfig/config.go new file mode 100644 index 0000000000..76372b561d --- /dev/null +++ b/services/rfq/guard/guardconfig/config.go @@ -0,0 +1,103 @@ +// Package guardconfig contains the config yaml object for the relayer. +package guardconfig + +import ( + "fmt" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/jftuga/ellipsis" + "github.com/synapsecns/sanguine/ethergo/signer/config" + submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" + "gopkg.in/yaml.v2" + + "path/filepath" +) + +// Config represents the configuration for the relayer. +type Config struct { + // Chains is a map of chainID -> chain config. + Chains map[int]ChainConfig `yaml:"chains"` + // OmniRPCURL is the URL of the OmniRPC server. + OmniRPCURL string `yaml:"omnirpc_url"` + // Database is the database config. + Database DatabaseConfig `yaml:"database"` + // Signer is the signer config. + Signer config.SignerConfig `yaml:"signer"` + // SubmitterConfig is the submitter config. + SubmitterConfig submitterConfig.Config `yaml:"submitter_config"` + // DBSelectorInterval is the interval for the db selector. + DBSelectorInterval time.Duration `yaml:"db_selector_interval"` +} + +// ChainConfig represents the configuration for a chain. +type ChainConfig struct { + // Bridge is the rfq bridge contract address. + RFQAddress string `yaml:"rfq_address"` + // Confirmations is the number of required confirmations. + Confirmations uint64 `yaml:"confirmations"` +} + +// DatabaseConfig represents the configuration for the database. +type DatabaseConfig struct { + Type string `yaml:"type"` + DSN string `yaml:"dsn"` // Data Source Name +} + +// LoadConfig loads the config from the given path. +func LoadConfig(path string) (config Config, err error) { + input, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return Config{}, fmt.Errorf("failed to read file: %w", err) + } + err = yaml.Unmarshal(input, &config) + if err != nil { + return Config{}, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(string(input), 30), err) + } + err = config.Validate() + if err != nil { + return config, fmt.Errorf("error validating config: %w", err) + } + return config, nil +} + +// Validate validates the config. +func (c Config) Validate() (err error) { + for chainID := range c.Chains { + addr, err := c.GetRFQAddress(chainID) + if err != nil { + return fmt.Errorf("could not get rfq address: %w", err) + } + if !common.IsHexAddress(addr) { + return fmt.Errorf("invalid rfq address: %s", addr) + } + } + + return nil +} + +// GetChains returns the chains config. +func (c Config) GetChains() map[int]ChainConfig { + return c.Chains +} + +// GetRFQAddress returns the RFQ address for the given chain. +func (c Config) GetRFQAddress(chainID int) (string, error) { + chainCfg, ok := c.Chains[chainID] + if !ok { + return "", fmt.Errorf("chain config not found for chain %d", chainID) + } + return chainCfg.RFQAddress, nil +} + +const defaultDBSelectorIntervalSeconds = 1 + +// GetDBSelectorInterval returns the interval for the DB selector. +func (c Config) GetDBSelectorInterval() time.Duration { + interval := c.DBSelectorInterval + if interval <= 0 { + interval = time.Duration(defaultDBSelectorIntervalSeconds) * time.Second + } + return interval +} diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 179a3bdfc6..b3f48977b0 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -18,9 +18,9 @@ import ( "github.com/synapsecns/sanguine/ethergo/submitter" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig" "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect" - "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" @@ -30,7 +30,7 @@ var logger = log.Logger("guard") // Guard monitors calls to prove() and verifies them. type Guard struct { - cfg relconfig.Config + cfg guardconfig.Config metrics metrics.Handler db guarddb.Service client omniClient.RPCClient @@ -40,7 +40,7 @@ type Guard struct { } // NewGuard creates a new Guard. -func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg relconfig.Config) (*Guard, error) { +func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfig.Config) (*Guard, error) { omniClient := omniClient.NewOmnirpcClient(cfg.OmniRPCURL, metricHandler, omniClient.WithCaptureReqRes()) chainListeners := make(map[int]listener.ContractListener) From 568accef70433347ab616f85d566c719d7b909be Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:53:21 -0500 Subject: [PATCH 16/37] Feat: add converter for relayer cfg -> guard cfg --- services/rfq/e2e/setup_test.go | 4 +++- services/rfq/guard/guardconfig/config.go | 20 ++++++++++++++++++++ services/rfq/relayer/service/relayer.go | 4 +++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index 9f13cf663b..bd24faa3f3 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -32,6 +32,7 @@ import ( "github.com/synapsecns/sanguine/services/rfq/api/db/sql" "github.com/synapsecns/sanguine/services/rfq/api/rest" "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20" + "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig" guardConnect "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect" guardService "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/chain" @@ -398,7 +399,8 @@ func (i *IntegrationSuite) setupGuard() { cfg := i.getRelayerConfig() var err error - i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, cfg) + guardCfg := guardconfig.NewGuardConfigFromRelayer(cfg) + i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg) i.NoError(err) go func() { err = i.guard.Start(i.GetTestContext()) diff --git a/services/rfq/guard/guardconfig/config.go b/services/rfq/guard/guardconfig/config.go index 76372b561d..eeed2f3a24 100644 --- a/services/rfq/guard/guardconfig/config.go +++ b/services/rfq/guard/guardconfig/config.go @@ -10,6 +10,7 @@ import ( "github.com/jftuga/ellipsis" "github.com/synapsecns/sanguine/ethergo/signer/config" submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" + "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "gopkg.in/yaml.v2" "path/filepath" @@ -101,3 +102,22 @@ func (c Config) GetDBSelectorInterval() time.Duration { } return interval } + +// NewGuardConfigFromRelayer creates a new guard config from a relayer config. +func NewGuardConfigFromRelayer(relayerCfg relconfig.Config) Config { + cfg := Config{ + Chains: make(map[int]ChainConfig), + OmniRPCURL: relayerCfg.OmniRPCURL, + Database: DatabaseConfig(relayerCfg.Database), + Signer: relayerCfg.Signer, + SubmitterConfig: relayerCfg.SubmitterConfig, + DBSelectorInterval: relayerCfg.DBSelectorInterval, + } + for chainID, chainCfg := range relayerCfg.GetChains() { + cfg.Chains[chainID] = ChainConfig{ + RFQAddress: chainCfg.RFQAddress, + Confirmations: chainCfg.Confirmations, + } + } + return cfg +} diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index 134aa32c26..116c71c036 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -26,6 +26,7 @@ import ( omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" rfqAPIClient "github.com/synapsecns/sanguine/services/rfq/api/client" "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig" serviceGuard "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/inventory" "github.com/synapsecns/sanguine/services/rfq/relayer/pricer" @@ -329,7 +330,8 @@ func (r *Relayer) startCCTPRelayer(ctx context.Context) (err error) { // startGuard starts the guard, if specified func (r *Relayer) startGuard(ctx context.Context) (err error) { - guard, err := serviceGuard.NewGuard(ctx, r.metrics, r.cfg) + guardCfg := guardconfig.NewGuardConfigFromRelayer(r.cfg) + guard, err := serviceGuard.NewGuard(ctx, r.metrics, guardCfg) if err != nil { return fmt.Errorf("could not create guard: %w", err) } From 24c551e98d86a2d5d32140b736f43e731c9ce2c9 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 13:57:26 -0500 Subject: [PATCH 17/37] Feat: add UseEmbeddedGuard flag --- services/rfq/relayer/relconfig/config.go | 2 ++ services/rfq/relayer/service/relayer.go | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index d0cdce49c6..f32864d279 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -55,6 +55,8 @@ type Config struct { EnableAPIWithdrawals bool `yaml:"enable_api_withdrawals"` // WithdrawalWhitelist is a list of addresses that are allowed to withdraw. WithdrawalWhitelist []string `yaml:"withdrawal_whitelist"` + // UseEmbeddedGuard enables the embedded guard. + UseEmbeddedGuard bool `yaml:"use_embedded_guard"` } // ChainConfig represents the configuration for a chain. diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index 116c71c036..b6302a94c6 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -268,6 +268,14 @@ func (r *Relayer) Start(ctx context.Context) (err error) { return nil }) + g.Go(func() error { + err = r.startGuard(ctx) + if err != nil { + return fmt.Errorf("could not start guard: %w", err) + } + return nil + }) + err = g.Wait() if err != nil { return fmt.Errorf("could not start: %w", err) @@ -330,6 +338,10 @@ func (r *Relayer) startCCTPRelayer(ctx context.Context) (err error) { // startGuard starts the guard, if specified func (r *Relayer) startGuard(ctx context.Context) (err error) { + if !r.cfg.UseEmbeddedGuard { + return nil + } + guardCfg := guardconfig.NewGuardConfigFromRelayer(r.cfg) guard, err := serviceGuard.NewGuard(ctx, r.metrics, guardCfg) if err != nil { From 99834deea70f2dab37697068c31f20887d687442 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 14:02:49 -0500 Subject: [PATCH 18/37] Feat: add guard wallet for e2e --- services/rfq/e2e/rfq_test.go | 1 + services/rfq/e2e/setup_test.go | 41 +++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index a365c9c9a5..6d81019e03 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -43,6 +43,7 @@ type IntegrationSuite struct { relayer *service.Relayer guard *guardService.Guard relayerWallet wallet.Wallet + guardWallet wallet.Wallet userWallet wallet.Wallet } diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index bd24faa3f3..7cb6da2523 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -97,6 +97,9 @@ func (i *IntegrationSuite) setupBackends() { i.relayerWallet, err = wallet.FromRandom() i.NoError(err) + i.guardWallet, err = wallet.FromRandom() + i.NoError(err) + i.userWallet, err = wallet.FromRandom() i.NoError(err) @@ -135,10 +138,12 @@ func (i *IntegrationSuite) setupBE(backend backends.SimulatedTestBackend) { // store the keys backend.Store(base.WalletToKey(i.T(), i.relayerWallet)) + backend.Store(base.WalletToKey(i.T(), i.guardWallet)) backend.Store(base.WalletToKey(i.T(), i.userWallet)) // fund each of the wallets backend.FundAccount(i.GetTestContext(), i.relayerWallet.Address(), ethAmount) + backend.FundAccount(i.GetTestContext(), i.guardWallet.Address(), ethAmount) backend.FundAccount(i.GetTestContext(), i.userWallet.Address(), ethAmount) go func() { @@ -147,7 +152,7 @@ func (i *IntegrationSuite) setupBE(backend backends.SimulatedTestBackend) { // TODO: in the case of relayer this not finishing before the test starts can lead to race conditions since // nonce may be shared between submitter and relayer. Think about how to deal w/ this. - for _, user := range []wallet.Wallet{i.relayerWallet, i.userWallet} { + for _, user := range []wallet.Wallet{i.relayerWallet, i.guardWallet, i.userWallet} { go func(userWallet wallet.Wallet) { for _, token := range predeployTokens { i.Approve(backend, i.manager.Get(i.GetTestContext(), backend, token), userWallet) @@ -396,18 +401,44 @@ func (i *IntegrationSuite) setupRelayer() { } func (i *IntegrationSuite) setupGuard() { - cfg := i.getRelayerConfig() + // add myself as a guard + var wg sync.WaitGroup + wg.Add(2) + + for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { + go func(backend backends.SimulatedTestBackend) { + defer wg.Done() + + metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend) + + txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr()) + guardRole, err := rfqContract.GUARDROLE(&bind.CallOpts{Context: i.GetTestContext()}) + i.NoError(err) + + tx, err := rfqContract.GrantRole(txContext.TransactOpts, guardRole, i.guardWallet.Address()) + i.NoError(err) + + backend.WaitForConfirmation(i.GetTestContext(), tx) + }(backend) + } + wg.Wait() + + relayerCfg := i.getRelayerConfig() + guardCfg := guardconfig.NewGuardConfigFromRelayer(relayerCfg) + guardCfg.Signer = signerConfig.SignerConfig{ + Type: signerConfig.FileType.String(), + File: filet.TmpFile(i.T(), "", i.guardWallet.PrivateKeyHex()).Name(), + } var err error - guardCfg := guardconfig.NewGuardConfigFromRelayer(cfg) i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg) i.NoError(err) go func() { err = i.guard.Start(i.GetTestContext()) }() - dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type) + dbType, err := dbcommon.DBTypeFromString(guardCfg.Database.Type) i.NoError(err) - i.guardStore, err = guardConnect.Connect(i.GetTestContext(), dbType, cfg.Database.DSN, i.metrics) + i.guardStore, err = guardConnect.Connect(i.GetTestContext(), dbType, guardCfg.Database.DSN, i.metrics) i.NoError(err) } From a9174c378deca77cdc9f0c9b3be2ded698f8a42c Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 14:23:50 -0500 Subject: [PATCH 19/37] WIP: add TestDispute --- services/rfq/e2e/rfq_test.go | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 6d81019e03..a7a6cad8c5 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -368,3 +368,96 @@ func (i *IntegrationSuite) TestETHtoETH() { return len(results) == 1 }) } + +func (i *IntegrationSuite) TestDispute() { + if core.GetEnvBool("CI", false) { + i.T().Skip("skipping until anvil issues are fixed in CI") + } + + // load token contracts + const startAmount = 1000 + const rfqAmount = 900 + opts := i.destBackend.GetTxContext(i.GetTestContext(), nil) + destUSDC, destUSDCHandle := i.cctpDeployManager.GetMockMintBurnTokenType(i.GetTestContext(), i.destBackend) + realStartAmount, err := testutil.AdjustAmount(i.GetTestContext(), big.NewInt(startAmount), destUSDC.ContractHandle()) + i.NoError(err) + realRFQAmount, err := testutil.AdjustAmount(i.GetTestContext(), big.NewInt(rfqAmount), destUSDC.ContractHandle()) + i.NoError(err) + + // add initial usdc to relayer on destination + tx, err := destUSDCHandle.MintPublic(opts.TransactOpts, i.relayerWallet.Address(), realStartAmount) + i.Nil(err) + i.destBackend.WaitForConfirmation(i.GetTestContext(), tx) + i.Approve(i.destBackend, destUSDC, i.relayerWallet) + + // add initial USDC to relayer on origin + optsOrigin := i.originBackend.GetTxContext(i.GetTestContext(), nil) + originUSDC, originUSDCHandle := i.cctpDeployManager.GetMockMintBurnTokenType(i.GetTestContext(), i.originBackend) + tx, err = originUSDCHandle.MintPublic(optsOrigin.TransactOpts, i.relayerWallet.Address(), realStartAmount) + i.Nil(err) + i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) + i.Approve(i.originBackend, originUSDC, i.relayerWallet) + + // add initial USDC to user on origin + tx, err = originUSDCHandle.MintPublic(optsOrigin.TransactOpts, i.userWallet.Address(), realRFQAmount) + i.Nil(err) + i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) + i.Approve(i.originBackend, originUSDC, i.userWallet) + + // now we can send the money + _, originFastBridge := i.manager.GetFastBridge(i.GetTestContext(), i.originBackend) + auth := i.originBackend.GetTxContext(i.GetTestContext(), i.userWallet.AddressPtr()) + // we want 499 usdc for 500 requested within a day + tx, err = originFastBridge.Bridge(auth.TransactOpts, fastbridge.IFastBridgeBridgeParams{ + DstChainId: uint32(i.destBackend.GetChainID()), + To: i.userWallet.Address(), + OriginToken: originUSDC.Address(), + SendChainGas: true, + DestToken: destUSDC.Address(), + OriginAmount: realRFQAmount, + DestAmount: new(big.Int).Sub(realRFQAmount, big.NewInt(10_000_000)), + Deadline: new(big.Int).SetInt64(time.Now().Add(time.Hour * 24).Unix()), + }) + i.NoError(err) + i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) + + // fetch the txID + var txID [32]byte + parser, err := fastbridge.NewParser(originFastBridge.Address()) + i.NoError(err) + i.Eventually(func() bool { + receipt, err := i.originBackend.TransactionReceipt(i.GetTestContext(), tx.Hash()) + i.NoError(err) + for _, log := range receipt.Logs { + _, parsedEvent, ok := parser.ParseEvent(*log) + if !ok { + continue + } + switch event := parsedEvent.(type) { + case *fastbridge.FastBridgeBridgeRequested: + txID = event.TransactionId + return true + } + } + return false + }) + + // call prove() from the relayer wallet before relay actually occurred on dest + relayerAuth := i.originBackend.GetTxContext(i.GetTestContext(), i.relayerWallet.AddressPtr()) + fakeHash := common.HexToHash("0xdeadbeef") + tx, err = originFastBridge.Prove(relayerAuth.TransactOpts, txID[:], fakeHash) + i.NoError(err) + i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) + + // verify that the guard calls Dispute() + i.Eventually(func() bool { + results, err := i.guardStore.GetPendingProvensByStatus(i.GetTestContext(), guarddb.Disputed) + i.NoError(err) + if len(results) != 1 { + return false + } + result, err := i.guardStore.GetPendingProvenByID(i.GetTestContext(), txID) + i.NoError(err) + return result.TxHash == fakeHash && result.Status == guarddb.Disputed && result.TransactionID == txID + }) +} From 11196b43b0b3e850373a285577bf088ddff04020 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 14:27:06 -0500 Subject: [PATCH 20/37] Feat: start relayer / guard within tests --- services/rfq/e2e/rfq_test.go | 22 ++++++++++++++++++++++ services/rfq/e2e/setup_test.go | 7 ------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index a7a6cad8c5..3469d7598c 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -106,6 +106,14 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { i.T().Skip("skipping until anvil issues are fixed in CI") } + // start the relayer and guard + go func() { + _ = i.relayer.Start(i.GetTestContext()) + }() + go func() { + _ = i.guard.Start(i.GetTestContext()) + }() + // load token contracts const startAmount = 1000 const rfqAmount = 900 @@ -260,6 +268,15 @@ func (i *IntegrationSuite) TestETHtoETH() { if core.GetEnvBool("CI", false) { i.T().Skip("skipping until anvil issues are fixed in CI") } + + // start the relayer and guard + go func() { + _ = i.relayer.Start(i.GetTestContext()) + }() + go func() { + _ = i.guard.Start(i.GetTestContext()) + }() + // Send ETH to the relayer on destination const initialBalance = 10 i.destBackend.FundAccount(i.GetTestContext(), i.relayerWallet.Address(), *big.NewInt(initialBalance)) @@ -374,6 +391,11 @@ func (i *IntegrationSuite) TestDispute() { i.T().Skip("skipping until anvil issues are fixed in CI") } + // start the guard + go func() { + _ = i.guard.Start(i.GetTestContext()) + }() + // load token contracts const startAmount = 1000 const rfqAmount = 900 diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index 7cb6da2523..8144af148c 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -386,13 +386,9 @@ func (i *IntegrationSuite) setupRelayer() { fmt.Sprintf("%d-%s", originBackendChainID, chain.EthAddress), } - // TODO: good chance we wanna leave actually starting this up to the indiividual test. var err error i.relayer, err = service.NewRelayer(i.GetTestContext(), i.metrics, cfg) i.NoError(err) - go func() { - err = i.relayer.Start(i.GetTestContext()) - }() dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type) i.NoError(err) @@ -433,9 +429,6 @@ func (i *IntegrationSuite) setupGuard() { var err error i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg) i.NoError(err) - go func() { - err = i.guard.Start(i.GetTestContext()) - }() dbType, err := dbcommon.DBTypeFromString(guardCfg.Database.Type) i.NoError(err) From 880e044c44877c9a6fd678a238254598fd025ddf Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 15:34:55 -0500 Subject: [PATCH 21/37] Fix: return valid=false on 'not found' err --- services/rfq/guard/service/guard.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index b3f48977b0..161591a232 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -2,10 +2,12 @@ package service import ( "context" + "errors" "fmt" "math/big" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -80,11 +82,7 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfi chainListeners[chainID] = chainListener // setup FastBridge contract on this chain - addr, err := cfg.GetRFQAddress(chainID) - if err != nil { - return nil, fmt.Errorf("could not get rfq address: %w", err) - } - contracts[chainID], err = fastbridge.NewFastBridgeRef(common.HexToAddress(addr), chainClient) + contracts[chainID], err = fastbridge.NewFastBridgeRef(common.HexToAddress(rfqAddr), chainClient) if err != nil { return nil, fmt.Errorf("could not create bridge contract: %w", err) } @@ -334,8 +332,11 @@ func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingPr return tx, nil }) if err != nil { - return fmt.Errorf("could not dispute: %w", err) + // return fmt.Errorf("could not dispute: %w", err) + fmt.Printf("DISPUTE ERR: %s\n", err.Error()) + return nil } + fmt.Printf("Submitted dispute!\n") // mark as dispute pending err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.DisputePending) @@ -354,6 +355,10 @@ func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, return false, fmt.Errorf("could not get chain client: %w", err) } receipt, err := chainClient.TransactionReceipt(ctx, proven.TxHash) + if errors.Is(err, ethereum.NotFound) { + // if tx hash does not exist, we want to consider the proof invalid + return false, nil + } if err != nil { return false, fmt.Errorf("could not get receipt: %w", err) } From 32ede77c5957edcb62b403e5ecb5c8d49f9b4576 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 15:35:01 -0500 Subject: [PATCH 22/37] Fix: pass in raw request --- services/rfq/e2e/rfq_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 3469d7598c..61413f6f1c 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -1,6 +1,7 @@ package e2e_test import ( + "fmt" "math/big" "testing" "time" @@ -445,6 +446,7 @@ func (i *IntegrationSuite) TestDispute() { // fetch the txID var txID [32]byte + var rawRequest []byte parser, err := fastbridge.NewParser(originFastBridge.Address()) i.NoError(err) i.Eventually(func() bool { @@ -457,6 +459,7 @@ func (i *IntegrationSuite) TestDispute() { } switch event := parsedEvent.(type) { case *fastbridge.FastBridgeBridgeRequested: + rawRequest = event.Request txID = event.TransactionId return true } @@ -467,7 +470,7 @@ func (i *IntegrationSuite) TestDispute() { // call prove() from the relayer wallet before relay actually occurred on dest relayerAuth := i.originBackend.GetTxContext(i.GetTestContext(), i.relayerWallet.AddressPtr()) fakeHash := common.HexToHash("0xdeadbeef") - tx, err = originFastBridge.Prove(relayerAuth.TransactOpts, txID[:], fakeHash) + tx, err = originFastBridge.Prove(relayerAuth.TransactOpts, rawRequest, fakeHash) i.NoError(err) i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) @@ -478,6 +481,7 @@ func (i *IntegrationSuite) TestDispute() { if len(results) != 1 { return false } + fmt.Printf("GOT RESULTS: %v\n", results) result, err := i.guardStore.GetPendingProvenByID(i.GetTestContext(), txID) i.NoError(err) return result.TxHash == fakeHash && result.Status == guarddb.Disputed && result.TransactionID == txID From 6ab95e121b0110c4c2085b3332cf2c2fb1f10f9d Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 15:57:27 -0500 Subject: [PATCH 23/37] Cleanup: logs --- services/rfq/guard/service/guard.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 161591a232..174b0da975 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -92,7 +92,6 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfi if err != nil { return nil, fmt.Errorf("could not get signer: %w", err) } - fmt.Printf("loaded signer with address: %s\n", sg.Address().String()) txSubmitter := submitter.NewTransactionSubmitter(metricHandler, sg, omniClient, store.SubmitterDB(), &cfg.SubmitterConfig) @@ -332,11 +331,8 @@ func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingPr return tx, nil }) if err != nil { - // return fmt.Errorf("could not dispute: %w", err) - fmt.Printf("DISPUTE ERR: %s\n", err.Error()) - return nil + return fmt.Errorf("could not dispute: %w", err) } - fmt.Printf("Submitted dispute!\n") // mark as dispute pending err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.DisputePending) From c08e4f52ce53282c589706b89d3914818e9577de Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 16:03:08 -0500 Subject: [PATCH 24/37] Feat: add tracing --- services/rfq/guard/service/guard.go | 41 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 174b0da975..0d32c87178 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ipfs/go-log" "github.com/synapsecns/sanguine/core/dbcommon" @@ -226,7 +227,15 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { var maxRPCRetryTime = 15 * time.Second -func (g *Guard) handleBridgeRequestedLog(ctx context.Context, req *fastbridge.FastBridgeBridgeRequested, chainID int) (err error) { +func (g *Guard) handleBridgeRequestedLog(parentCtx context.Context, req *fastbridge.FastBridgeBridgeRequested, chainID int) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleBridgeRequestedLog-guard", trace.WithAttributes( + attribute.Int(metrics.Origin, chainID), + attribute.String("transaction_id", hexutil.Encode(req.TransactionId[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + originClient, err := g.client.GetChainClient(ctx, int(chainID)) if err != nil { return fmt.Errorf("could not get correct omnirpc client: %w", err) @@ -262,7 +271,16 @@ func (g *Guard) handleBridgeRequestedLog(ctx context.Context, req *fastbridge.Fa return nil } -func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { +func (g *Guard) handleProofProvidedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofProvidedLog-guard", trace.WithAttributes( + attribute.Int(metrics.Origin, chainID), + attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), + attribute.String("tx_hash", hexutil.Encode(event.TransactionHash[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + proven := guarddb.PendingProven{ Origin: uint32(chainID), TransactionID: event.TransactionId, @@ -277,7 +295,14 @@ func (g *Guard) handleProofProvidedLog(ctx context.Context, event *fastbridge.Fa return nil } -func (g *Guard) handleProofDisputedLog(ctx context.Context, event *fastbridge.FastBridgeBridgeProofDisputed) (err error) { +func (g *Guard) handleProofDisputedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofDisputed) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofDisputedLog-guard", trace.WithAttributes( + attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + err = g.db.UpdatePendingProvenStatus(ctx, event.TransactionId, guarddb.Disputed) if err != nil { return fmt.Errorf("could not update pending proven status: %w", err) @@ -298,7 +323,14 @@ func (g *Guard) processDB(ctx context.Context) (err error) { return nil } -func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingProven) (err error) { +func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.PendingProven) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProveCalled", trace.WithAttributes( + attribute.String("transaction_id", hexutil.Encode(proven.TransactionID[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + // first, get the corresponding bridge request bridgeRequest, err := g.db.GetBridgeRequestByID(ctx, proven.TransactionID) if err != nil { @@ -309,6 +341,7 @@ func (g *Guard) handleProveCalled(ctx context.Context, proven *guarddb.PendingPr if err != nil { return fmt.Errorf("could not check prove validity: %w", err) } + span.SetAttributes(attribute.Bool("valid", valid)) if valid { // mark as validated From b34469440eb42941decd1a224a450a2a8163b152 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 16:06:20 -0500 Subject: [PATCH 25/37] Cleanup: move handlers to handlers.go --- services/rfq/guard/service/guard.go | 209 ----------------------- services/rfq/guard/service/handlers.go | 225 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 209 deletions(-) create mode 100644 services/rfq/guard/service/handlers.go diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 0d32c87178..76650f2dfb 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -2,20 +2,15 @@ package service import ( "context" - "errors" "fmt" - "math/big" "time" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ipfs/go-log" "github.com/synapsecns/sanguine/core/dbcommon" "github.com/synapsecns/sanguine/core/metrics" - "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/ethergo/listener" signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" "github.com/synapsecns/sanguine/ethergo/submitter" @@ -225,92 +220,6 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { return nil } -var maxRPCRetryTime = 15 * time.Second - -func (g *Guard) handleBridgeRequestedLog(parentCtx context.Context, req *fastbridge.FastBridgeBridgeRequested, chainID int) (err error) { - ctx, span := g.metrics.Tracer().Start(parentCtx, "handleBridgeRequestedLog-guard", trace.WithAttributes( - attribute.Int(metrics.Origin, chainID), - attribute.String("transaction_id", hexutil.Encode(req.TransactionId[:])), - )) - defer func() { - metrics.EndSpanWithErr(span, err) - }() - - originClient, err := g.client.GetChainClient(ctx, int(chainID)) - if err != nil { - return fmt.Errorf("could not get correct omnirpc client: %w", err) - } - - fastBridge, err := fastbridge.NewFastBridgeRef(req.Raw.Address, originClient) - if err != nil { - return fmt.Errorf("could not get correct fast bridge: %w", err) - } - - var bridgeTx fastbridge.IFastBridgeBridgeTransaction - call := func(ctx context.Context) error { - bridgeTx, err = fastBridge.GetBridgeTransaction(&bind.CallOpts{Context: ctx}, req.Request) - if err != nil { - return fmt.Errorf("could not get bridge transaction: %w", err) - } - return nil - } - err = retry.WithBackoff(ctx, call, retry.WithMaxTotalTime(maxRPCRetryTime)) - if err != nil { - return fmt.Errorf("could not make call: %w", err) - } - - dbReq := guarddb.BridgeRequest{ - RawRequest: req.Request, - TransactionID: req.TransactionId, - Transaction: bridgeTx, - } - err = g.db.StoreBridgeRequest(ctx, dbReq) - if err != nil { - return fmt.Errorf("could not get db: %w", err) - } - return nil -} - -func (g *Guard) handleProofProvidedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { - ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofProvidedLog-guard", trace.WithAttributes( - attribute.Int(metrics.Origin, chainID), - attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), - attribute.String("tx_hash", hexutil.Encode(event.TransactionHash[:])), - )) - defer func() { - metrics.EndSpanWithErr(span, err) - }() - - proven := guarddb.PendingProven{ - Origin: uint32(chainID), - TransactionID: event.TransactionId, - TxHash: event.TransactionHash, - Status: guarddb.ProveCalled, - } - err = g.db.StorePendingProven(ctx, proven) - if err != nil { - return fmt.Errorf("could not store pending proven: %w", err) - } - - return nil -} - -func (g *Guard) handleProofDisputedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofDisputed) (err error) { - ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofDisputedLog-guard", trace.WithAttributes( - attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), - )) - defer func() { - metrics.EndSpanWithErr(span, err) - }() - - err = g.db.UpdatePendingProvenStatus(ctx, event.TransactionId, guarddb.Disputed) - if err != nil { - return fmt.Errorf("could not update pending proven status: %w", err) - } - - return nil -} - func (g *Guard) processDB(ctx context.Context) (err error) { provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.ProveCalled) for _, proven := range provens { @@ -322,121 +231,3 @@ func (g *Guard) processDB(ctx context.Context) (err error) { return nil } - -func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.PendingProven) (err error) { - ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProveCalled", trace.WithAttributes( - attribute.String("transaction_id", hexutil.Encode(proven.TransactionID[:])), - )) - defer func() { - metrics.EndSpanWithErr(span, err) - }() - - // first, get the corresponding bridge request - bridgeRequest, err := g.db.GetBridgeRequestByID(ctx, proven.TransactionID) - if err != nil { - return fmt.Errorf("could not get bridge request: %w", err) - } - - valid, err := g.isProveValid(ctx, proven, bridgeRequest) - if err != nil { - return fmt.Errorf("could not check prove validity: %w", err) - } - span.SetAttributes(attribute.Bool("valid", valid)) - - if valid { - // mark as validated - err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Validated) - if err != nil { - return fmt.Errorf("could not update pending proven status: %w", err) - } - } else { - // trigger dispute - contract, ok := g.contracts[int(bridgeRequest.Transaction.DestChainId)] - if !ok { - return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.DestChainId) - } - _, err := g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(proven.Origin)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - tx, err = contract.Dispute(&bind.TransactOpts{Context: ctx}, proven.TransactionID) - if err != nil { - return nil, fmt.Errorf("could not dispute: %w", err) - } - - return tx, nil - }) - if err != nil { - return fmt.Errorf("could not dispute: %w", err) - } - - // mark as dispute pending - err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.DisputePending) - if err != nil { - return fmt.Errorf("could not update pending proven status: %w", err) - } - } - - return nil -} - -func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, bridgeRequest *guarddb.BridgeRequest) (bool, error) { - // get the receipt for this tx on dest chain - chainClient, err := g.client.GetChainClient(ctx, int(bridgeRequest.Transaction.DestChainId)) - if err != nil { - return false, fmt.Errorf("could not get chain client: %w", err) - } - receipt, err := chainClient.TransactionReceipt(ctx, proven.TxHash) - if errors.Is(err, ethereum.NotFound) { - // if tx hash does not exist, we want to consider the proof invalid - return false, nil - } - if err != nil { - return false, fmt.Errorf("could not get receipt: %w", err) - } - addr, err := g.cfg.GetRFQAddress(int(bridgeRequest.Transaction.DestChainId)) - if err != nil { - return false, fmt.Errorf("could not get rfq address: %w", err) - } - parser, err := fastbridge.NewParser(common.HexToAddress(addr)) - if err != nil { - return false, fmt.Errorf("could not get parser: %w", err) - } - - for _, log := range receipt.Logs { - _, parsedEvent, ok := parser.ParseEvent(*log) - if !ok { - continue - } - - switch event := parsedEvent.(type) { - case *fastbridge.FastBridgeBridgeRelayed: - return relayMatchesBridgeRequest(event, bridgeRequest), nil - } - } - - return false, nil -} - -func relayMatchesBridgeRequest(event *fastbridge.FastBridgeBridgeRelayed, bridgeRequest *guarddb.BridgeRequest) bool { - //TODO: is this exhaustive? - if event.TransactionId != bridgeRequest.TransactionID { - return false - } - if event.OriginAmount.Cmp(bridgeRequest.Transaction.OriginAmount) != 0 { - return false - } - if event.DestAmount.Cmp(bridgeRequest.Transaction.DestAmount) != 0 { - return false - } - if event.OriginChainId != bridgeRequest.Transaction.OriginChainId { - return false - } - if event.To != bridgeRequest.Transaction.DestRecipient { - return false - } - if event.OriginToken != bridgeRequest.Transaction.OriginToken { - return false - } - if event.DestToken != bridgeRequest.Transaction.DestToken { - return false - } - return true -} diff --git a/services/rfq/guard/service/handlers.go b/services/rfq/guard/service/handlers.go new file mode 100644 index 0000000000..52d631d036 --- /dev/null +++ b/services/rfq/guard/service/handlers.go @@ -0,0 +1,225 @@ +package service + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/core/metrics" + "github.com/synapsecns/sanguine/core/retry" + "github.com/synapsecns/sanguine/services/rfq/contracts/fastbridge" + "github.com/synapsecns/sanguine/services/rfq/guard/guarddb" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +var maxRPCRetryTime = 15 * time.Second + +func (g *Guard) handleBridgeRequestedLog(parentCtx context.Context, req *fastbridge.FastBridgeBridgeRequested, chainID int) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleBridgeRequestedLog-guard", trace.WithAttributes( + attribute.Int(metrics.Origin, chainID), + attribute.String("transaction_id", hexutil.Encode(req.TransactionId[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + originClient, err := g.client.GetChainClient(ctx, int(chainID)) + if err != nil { + return fmt.Errorf("could not get correct omnirpc client: %w", err) + } + + fastBridge, err := fastbridge.NewFastBridgeRef(req.Raw.Address, originClient) + if err != nil { + return fmt.Errorf("could not get correct fast bridge: %w", err) + } + + var bridgeTx fastbridge.IFastBridgeBridgeTransaction + call := func(ctx context.Context) error { + bridgeTx, err = fastBridge.GetBridgeTransaction(&bind.CallOpts{Context: ctx}, req.Request) + if err != nil { + return fmt.Errorf("could not get bridge transaction: %w", err) + } + return nil + } + err = retry.WithBackoff(ctx, call, retry.WithMaxTotalTime(maxRPCRetryTime)) + if err != nil { + return fmt.Errorf("could not make call: %w", err) + } + + dbReq := guarddb.BridgeRequest{ + RawRequest: req.Request, + TransactionID: req.TransactionId, + Transaction: bridgeTx, + } + err = g.db.StoreBridgeRequest(ctx, dbReq) + if err != nil { + return fmt.Errorf("could not get db: %w", err) + } + return nil +} + +func (g *Guard) handleProofProvidedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofProvided, chainID int) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofProvidedLog-guard", trace.WithAttributes( + attribute.Int(metrics.Origin, chainID), + attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), + attribute.String("tx_hash", hexutil.Encode(event.TransactionHash[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + proven := guarddb.PendingProven{ + Origin: uint32(chainID), + TransactionID: event.TransactionId, + TxHash: event.TransactionHash, + Status: guarddb.ProveCalled, + } + err = g.db.StorePendingProven(ctx, proven) + if err != nil { + return fmt.Errorf("could not store pending proven: %w", err) + } + + return nil +} + +func (g *Guard) handleProofDisputedLog(parentCtx context.Context, event *fastbridge.FastBridgeBridgeProofDisputed) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProofDisputedLog-guard", trace.WithAttributes( + attribute.String("transaction_id", hexutil.Encode(event.TransactionId[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + err = g.db.UpdatePendingProvenStatus(ctx, event.TransactionId, guarddb.Disputed) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + + return nil +} + +func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.PendingProven) (err error) { + ctx, span := g.metrics.Tracer().Start(parentCtx, "handleProveCalled", trace.WithAttributes( + attribute.String("transaction_id", hexutil.Encode(proven.TransactionID[:])), + )) + defer func() { + metrics.EndSpanWithErr(span, err) + }() + + // first, get the corresponding bridge request + bridgeRequest, err := g.db.GetBridgeRequestByID(ctx, proven.TransactionID) + if err != nil { + return fmt.Errorf("could not get bridge request: %w", err) + } + + valid, err := g.isProveValid(ctx, proven, bridgeRequest) + if err != nil { + return fmt.Errorf("could not check prove validity: %w", err) + } + span.SetAttributes(attribute.Bool("valid", valid)) + + if valid { + // mark as validated + err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Validated) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + } else { + // trigger dispute + contract, ok := g.contracts[int(bridgeRequest.Transaction.DestChainId)] + if !ok { + return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.DestChainId) + } + _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(proven.Origin)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + tx, err = contract.Dispute(&bind.TransactOpts{Context: ctx}, proven.TransactionID) + if err != nil { + return nil, fmt.Errorf("could not dispute: %w", err) + } + + return tx, nil + }) + if err != nil { + return fmt.Errorf("could not dispute: %w", err) + } + + // mark as dispute pending + err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.DisputePending) + if err != nil { + return fmt.Errorf("could not update pending proven status: %w", err) + } + } + + return nil +} + +func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, bridgeRequest *guarddb.BridgeRequest) (bool, error) { + // get the receipt for this tx on dest chain + chainClient, err := g.client.GetChainClient(ctx, int(bridgeRequest.Transaction.DestChainId)) + if err != nil { + return false, fmt.Errorf("could not get chain client: %w", err) + } + receipt, err := chainClient.TransactionReceipt(ctx, proven.TxHash) + if errors.Is(err, ethereum.NotFound) { + // if tx hash does not exist, we want to consider the proof invalid + return false, nil + } + if err != nil { + return false, fmt.Errorf("could not get receipt: %w", err) + } + addr, err := g.cfg.GetRFQAddress(int(bridgeRequest.Transaction.DestChainId)) + if err != nil { + return false, fmt.Errorf("could not get rfq address: %w", err) + } + parser, err := fastbridge.NewParser(common.HexToAddress(addr)) + if err != nil { + return false, fmt.Errorf("could not get parser: %w", err) + } + + for _, log := range receipt.Logs { + _, parsedEvent, ok := parser.ParseEvent(*log) + if !ok { + continue + } + + switch event := parsedEvent.(type) { + case *fastbridge.FastBridgeBridgeRelayed: + return relayMatchesBridgeRequest(event, bridgeRequest), nil + } + } + + return false, nil +} + +func relayMatchesBridgeRequest(event *fastbridge.FastBridgeBridgeRelayed, bridgeRequest *guarddb.BridgeRequest) bool { + //TODO: is this exhaustive? + if event.TransactionId != bridgeRequest.TransactionID { + return false + } + if event.OriginAmount.Cmp(bridgeRequest.Transaction.OriginAmount) != 0 { + return false + } + if event.DestAmount.Cmp(bridgeRequest.Transaction.DestAmount) != 0 { + return false + } + if event.OriginChainId != bridgeRequest.Transaction.OriginChainId { + return false + } + if event.To != bridgeRequest.Transaction.DestRecipient { + return false + } + if event.OriginToken != bridgeRequest.Transaction.OriginToken { + return false + } + if event.DestToken != bridgeRequest.Transaction.DestToken { + return false + } + return true +} From cdd769df0f3e1b14c19165142b383a4e2a2fd840 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Wed, 3 Jul 2024 20:15:45 -0500 Subject: [PATCH 26/37] Cleanup: lint --- services/rfq/e2e/rfq_test.go | 6 +++--- services/rfq/guard/guarddb/base/model.go | 25 ++---------------------- services/rfq/guard/service/doc.go | 2 ++ 3 files changed, 7 insertions(+), 26 deletions(-) create mode 100644 services/rfq/guard/service/doc.go diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 61413f6f1c..3275c9a1c7 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -444,7 +444,7 @@ func (i *IntegrationSuite) TestDispute() { i.NoError(err) i.originBackend.WaitForConfirmation(i.GetTestContext(), tx) - // fetch the txID + // fetch the txid and raw request var txID [32]byte var rawRequest []byte parser, err := fastbridge.NewParser(originFastBridge.Address()) @@ -457,8 +457,8 @@ func (i *IntegrationSuite) TestDispute() { if !ok { continue } - switch event := parsedEvent.(type) { - case *fastbridge.FastBridgeBridgeRequested: + event, ok := parsedEvent.(*fastbridge.FastBridgeBridgeRequested) + if ok { rawRequest = event.Request txID = event.TransactionId return true diff --git a/services/rfq/guard/guarddb/base/model.go b/services/rfq/guard/guarddb/base/model.go index 1e2b48d059..b8dd1795ef 100644 --- a/services/rfq/guard/guarddb/base/model.go +++ b/services/rfq/guard/guarddb/base/model.go @@ -1,7 +1,6 @@ package base import ( - "database/sql" "errors" "fmt" "math/big" @@ -53,28 +52,6 @@ func FromPendingProven(proven guarddb.PendingProven) PendingProvenModel { } } -var emptyHash = common.HexToHash("").Hex() - -func hashToNullString(h common.Hash) sql.NullString { - if h.Hex() == emptyHash { - return sql.NullString{Valid: false} - } - return sql.NullString{ - String: h.Hex(), - Valid: true, - } -} - -func stringToNullString(s string) sql.NullString { - if s == "" { - return sql.NullString{Valid: false} - } - return sql.NullString{ - String: s, - Valid: true, - } -} - // ToPendingProven converts a db object to a pending proven. func (p PendingProvenModel) ToPendingProven() (*guarddb.PendingProven, error) { txID, err := hexutil.Decode(p.TransactionID) @@ -131,6 +108,7 @@ type BridgeRequestModel struct { SendChainGas bool } +// FromBridgeRequest converts a bridge request object to db model. func FromBridgeRequest(request guarddb.BridgeRequest) BridgeRequestModel { return BridgeRequestModel{ TransactionID: hexutil.Encode(request.TransactionID[:]), @@ -149,6 +127,7 @@ func FromBridgeRequest(request guarddb.BridgeRequest) BridgeRequestModel { } } +// ToBridgeRequest converts the bridge request db model to object. func (b BridgeRequestModel) ToBridgeRequest() (*guarddb.BridgeRequest, error) { txID, err := hexutil.Decode(b.TransactionID) if err != nil { diff --git a/services/rfq/guard/service/doc.go b/services/rfq/guard/service/doc.go new file mode 100644 index 0000000000..30310f7a59 --- /dev/null +++ b/services/rfq/guard/service/doc.go @@ -0,0 +1,2 @@ +// Package service contains the core of the guard. +package service From bb677e0c3b555e1c7f7df618e307f99b2f1b2dfe Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Fri, 5 Jul 2024 11:26:21 -0500 Subject: [PATCH 27/37] Cleanup: lint --- services/rfq/guard/service/guard.go | 17 ++++++++++++++--- services/rfq/guard/service/handlers.go | 7 ++++--- services/rfq/relayer/service/relayer.go | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 76650f2dfb..4926008507 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -38,6 +38,8 @@ type Guard struct { } // NewGuard creates a new Guard. +// +//nolint:cyclop func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfig.Config) (*Guard, error) { omniClient := omniClient.NewOmnirpcClient(cfg.OmniRPCURL, metricHandler, omniClient.WithCaptureReqRes()) chainListeners := make(map[int]listener.ContractListener) @@ -102,8 +104,6 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfi }, nil } -const defaultDBInterval = 5 - // Start starts the guard. func (g *Guard) Start(ctx context.Context) (err error) { group, ctx := errgroup.WithContext(ctx) @@ -146,7 +146,7 @@ func (g *Guard) runDBSelector(ctx context.Context) (err error) { } } -func (g *Guard) startChainIndexers(ctx context.Context) error { +func (g *Guard) startChainIndexers(ctx context.Context) (err error) { group, ctx := errgroup.WithContext(ctx) for chainID := range g.cfg.GetChains() { @@ -160,9 +160,16 @@ func (g *Guard) startChainIndexers(ctx context.Context) error { return nil }) } + + err = group.Wait() + if err != nil { + return fmt.Errorf("could not run chain indexers") + } + return nil } +//nolint:cyclop func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { chainListener := g.chainListeners[chainID] @@ -222,6 +229,10 @@ func (g Guard) runChainIndexer(ctx context.Context, chainID int) (err error) { func (g *Guard) processDB(ctx context.Context) (err error) { provens, err := g.db.GetPendingProvensByStatus(ctx, guarddb.ProveCalled) + if err != nil { + return fmt.Errorf("could not get pending provens: %w", err) + } + for _, proven := range provens { err := g.handleProveCalled(ctx, proven) if err != nil { diff --git a/services/rfq/guard/service/handlers.go b/services/rfq/guard/service/handlers.go index 52d631d036..4dd42fdf63 100644 --- a/services/rfq/guard/service/handlers.go +++ b/services/rfq/guard/service/handlers.go @@ -31,7 +31,7 @@ func (g *Guard) handleBridgeRequestedLog(parentCtx context.Context, req *fastbri metrics.EndSpanWithErr(span, err) }() - originClient, err := g.client.GetChainClient(ctx, int(chainID)) + originClient, err := g.client.GetChainClient(ctx, chainID) if err != nil { return fmt.Errorf("could not get correct omnirpc client: %w", err) } @@ -126,6 +126,7 @@ func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.Pen } span.SetAttributes(attribute.Bool("valid", valid)) + //nolint:nestif if valid { // mark as validated err = g.db.UpdatePendingProvenStatus(ctx, proven.TransactionID, guarddb.Validated) @@ -189,8 +190,8 @@ func (g *Guard) isProveValid(ctx context.Context, proven *guarddb.PendingProven, continue } - switch event := parsedEvent.(type) { - case *fastbridge.FastBridgeBridgeRelayed: + event, ok := parsedEvent.(*fastbridge.FastBridgeBridgeRelayed) + if ok { return relayMatchesBridgeRequest(event, bridgeRequest), nil } } diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index b6302a94c6..fd377e26f6 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -336,7 +336,7 @@ func (r *Relayer) startCCTPRelayer(ctx context.Context) (err error) { return nil } -// startGuard starts the guard, if specified +// startGuard starts the guard, if specified. func (r *Relayer) startGuard(ctx context.Context) (err error) { if !r.cfg.UseEmbeddedGuard { return nil From ed153b77091fe1f2ef30cafb0191e61ed3db2e7d Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Fri, 5 Jul 2024 11:26:25 -0500 Subject: [PATCH 28/37] [goreleaser] From afb427b773942589b3f710267164013efd799a43 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Fri, 5 Jul 2024 11:58:42 -0500 Subject: [PATCH 29/37] [goreleaser] From 6efd1002ed0bee2e8b55dd610dee6bc59b140612 Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Fri, 5 Jul 2024 12:12:52 -0500 Subject: [PATCH 30/37] Fix: inherit txSubmitter from relayer --- services/rfq/e2e/setup_test.go | 2 +- services/rfq/guard/cmd/commands.go | 2 +- services/rfq/guard/service/guard.go | 14 ++++++++------ services/rfq/relayer/service/relayer.go | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index 8144af148c..bed24ccc7f 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -427,7 +427,7 @@ func (i *IntegrationSuite) setupGuard() { } var err error - i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg) + i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg, nil) i.NoError(err) dbType, err := dbcommon.DBTypeFromString(guardCfg.Database.Type) diff --git a/services/rfq/guard/cmd/commands.go b/services/rfq/guard/cmd/commands.go index eab8721b22..48ff0a46cb 100644 --- a/services/rfq/guard/cmd/commands.go +++ b/services/rfq/guard/cmd/commands.go @@ -32,7 +32,7 @@ var runCommand = &cli.Command{ metricsProvider := metrics.Get() - guard, err := service.NewGuard(c.Context, metricsProvider, cfg) + guard, err := service.NewGuard(c.Context, metricsProvider, cfg, nil) if err != nil { return fmt.Errorf("could not create guard: %w", err) } diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 4926008507..33eaa5727f 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -40,7 +40,7 @@ type Guard struct { // NewGuard creates a new Guard. // //nolint:cyclop -func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfig.Config) (*Guard, error) { +func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfig.Config, txSubmitter submitter.TransactionSubmitter) (*Guard, error) { omniClient := omniClient.NewOmnirpcClient(cfg.OmniRPCURL, metricHandler, omniClient.WithCaptureReqRes()) chainListeners := make(map[int]listener.ContractListener) @@ -86,13 +86,15 @@ func NewGuard(ctx context.Context, metricHandler metrics.Handler, cfg guardconfi } } - sg, err := signerConfig.SignerFromConfig(ctx, cfg.Signer) - if err != nil { - return nil, fmt.Errorf("could not get signer: %w", err) + // build submitter from config if one is not supplied + if txSubmitter == nil { + sg, err := signerConfig.SignerFromConfig(ctx, cfg.Signer) + if err != nil { + return nil, fmt.Errorf("could not get signer: %w", err) + } + txSubmitter = submitter.NewTransactionSubmitter(metricHandler, sg, omniClient, store.SubmitterDB(), &cfg.SubmitterConfig) } - txSubmitter := submitter.NewTransactionSubmitter(metricHandler, sg, omniClient, store.SubmitterDB(), &cfg.SubmitterConfig) - return &Guard{ cfg: cfg, metrics: metricHandler, diff --git a/services/rfq/relayer/service/relayer.go b/services/rfq/relayer/service/relayer.go index fd377e26f6..5a95e3fd97 100644 --- a/services/rfq/relayer/service/relayer.go +++ b/services/rfq/relayer/service/relayer.go @@ -343,7 +343,7 @@ func (r *Relayer) startGuard(ctx context.Context) (err error) { } guardCfg := guardconfig.NewGuardConfigFromRelayer(r.cfg) - guard, err := serviceGuard.NewGuard(ctx, r.metrics, guardCfg) + guard, err := serviceGuard.NewGuard(ctx, r.metrics, guardCfg, r.submitter) if err != nil { return fmt.Errorf("could not create guard: %w", err) } From f5a00e6a9205f14c02c32c324af01fdfc64c92fb Mon Sep 17 00:00:00 2001 From: Daniel Wasserman Date: Fri, 5 Jul 2024 12:14:44 -0500 Subject: [PATCH 31/37] [goreleaser] From d3011d810a80ffa1ba441004beae0761a9cbb4bb Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Fri, 5 Jul 2024 16:57:52 -0400 Subject: [PATCH 32/37] add guard --- docs/bridge/docs/rfq/Relayer/Relayer.md | 1 + services/cctp-relayer/external/evm-cctp-contracts | 2 +- services/rfq/guard/guarddb/doc.go | 2 +- services/rfq/guard/guarddb/mysql/mysql.go | 4 ++-- services/rfq/relayer/relconfig/config.go | 2 +- services/rfq/relayer/reldb/doc.go | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/bridge/docs/rfq/Relayer/Relayer.md b/docs/bridge/docs/rfq/Relayer/Relayer.md index 49555381c9..27ac16268c 100644 --- a/docs/bridge/docs/rfq/Relayer/Relayer.md +++ b/docs/bridge/docs/rfq/Relayer/Relayer.md @@ -193,6 +193,7 @@ The relayer is configured with a yaml file. The following is an example configur - `rebalance_interval` - How often to rebalance, formatted as (s = seconds, m = minutes, h = hours) - `relayer_api_port` - the relayer api is used to control the relayer. This api should be secured/not public. - `base_chain_config`: Base chain config is the default config applied for each chain if the other chains do not override it. This is covered in the chains section. + - `enable_embedded_guard` - Run a guard on the same instance. - `chains` - each chain has a different config that overrides base_chain_config. Here are the parameters for each chain - `rfq_address` - the address of the rfq contract on this chain. These addresses are available [here](../Contracts.md). diff --git a/services/cctp-relayer/external/evm-cctp-contracts b/services/cctp-relayer/external/evm-cctp-contracts index 377c9bd813..817397db0a 160000 --- a/services/cctp-relayer/external/evm-cctp-contracts +++ b/services/cctp-relayer/external/evm-cctp-contracts @@ -1 +1 @@ -Subproject commit 377c9bd813fb86a42d900ae4003599d82aef635a +Subproject commit 817397db0a12963accc08ff86065491577bbc0e5 diff --git a/services/rfq/guard/guarddb/doc.go b/services/rfq/guard/guarddb/doc.go index 2efd9af622..c81d904fed 100644 --- a/services/rfq/guard/guarddb/doc.go +++ b/services/rfq/guard/guarddb/doc.go @@ -1,3 +1,3 @@ -// Package guarddb contains the datbaase interface for the rfq guard. +// Package guarddb contains the database interface for the rfq guard. // All data store types must confrm to this interface. package guarddb diff --git a/services/rfq/guard/guarddb/mysql/mysql.go b/services/rfq/guard/guarddb/mysql/mysql.go index 560a0a6c65..0048e56638 100644 --- a/services/rfq/guard/guarddb/mysql/mysql.go +++ b/services/rfq/guard/guarddb/mysql/mysql.go @@ -1,4 +1,4 @@ -// Package mysql provides a common interface for starting sql-lite databases +// Package mysql provides a common interface for starting mysql databases package mysql import ( @@ -17,7 +17,7 @@ import ( var logger = log.Logger("mysql-logger") -// Store is the sqlite store. It extends the base store for sqlite specific queries. +// Store is the mysql store. It extends the base store for mysql specific queries. type Store struct { *base.Store } diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index f32864d279..7c8d644d92 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -56,7 +56,7 @@ type Config struct { // WithdrawalWhitelist is a list of addresses that are allowed to withdraw. WithdrawalWhitelist []string `yaml:"withdrawal_whitelist"` // UseEmbeddedGuard enables the embedded guard. - UseEmbeddedGuard bool `yaml:"use_embedded_guard"` + UseEmbeddedGuard bool `yaml:"enable_guard"` } // ChainConfig represents the configuration for a chain. diff --git a/services/rfq/relayer/reldb/doc.go b/services/rfq/relayer/reldb/doc.go index eceacfd65d..41125227cc 100644 --- a/services/rfq/relayer/reldb/doc.go +++ b/services/rfq/relayer/reldb/doc.go @@ -1,3 +1,3 @@ -// Package reldb contains the datbaase interface for the rfq relayer. +// Package reldb contains the database interface for the rfq relayer. // All data store types must confrm to this interface. package reldb From 6982d446a546e2e4a9fc8e3725ff9117b09fd9c0 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Fri, 5 Jul 2024 17:00:36 -0400 Subject: [PATCH 33/37] use correct transactor --- services/rfq/guard/service/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/rfq/guard/service/handlers.go b/services/rfq/guard/service/handlers.go index 4dd42fdf63..26c2b9dd52 100644 --- a/services/rfq/guard/service/handlers.go +++ b/services/rfq/guard/service/handlers.go @@ -140,7 +140,7 @@ func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.Pen return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.DestChainId) } _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(proven.Origin)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { - tx, err = contract.Dispute(&bind.TransactOpts{Context: ctx}, proven.TransactionID) + tx, err = contract.Dispute(transactor, proven.TransactionID) if err != nil { return nil, fmt.Errorf("could not dispute: %w", err) } From 9b22148e54325aa5efe1f6fcc31ded283b83be2e Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 6 Jul 2024 00:16:03 -0400 Subject: [PATCH 34/37] event parsing --- ethergo/submitter/submitter.go | 40 +++++++++++++++++++ services/rfq/contracts/fastbridge/events.go | 9 +++++ .../contracts/fastbridge/eventtype_string.go | 5 ++- services/rfq/contracts/fastbridge/parser.go | 9 +++++ services/rfq/guard/service/guard.go | 12 ++++++ services/rfq/guard/service/handlers.go | 7 ++-- 6 files changed, 77 insertions(+), 5 deletions(-) diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index 0823d690cc..b79dfecb32 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -48,6 +48,8 @@ type TransactionSubmitter interface { GetSubmissionStatus(ctx context.Context, chainID *big.Int, nonce uint64) (status SubmissionStatus, err error) // Address returns the address of the signer. Address() common.Address + // Started returns whether the submitter is running. + Started() bool } // txSubmitterImpl is the implementation of the transaction submitter. @@ -83,6 +85,10 @@ type txSubmitterImpl struct { // distinctChainIDs is the distinct chain ids for the transaction submitter. // note: this map should not be appended to! distinctChainIDs []*big.Int + // started indicates whether the submitter has started. + started bool + // startMux is the mutex for started. + startMux sync.RWMutex } // ClientFetcher is the interface for fetching a chain client. @@ -107,6 +113,13 @@ func NewTransactionSubmitter(metrics metrics.Handler, signer signer.Signer, fetc } } +// Started returns whether the submitter is running. +func (t *txSubmitterImpl) Started() bool { + t.startMux.RLock() + defer t.startMux.RUnlock() + return t.started +} + // GetRetryInterval returns the retry interval for the transaction submitter. func (t *txSubmitterImpl) GetRetryInterval() time.Duration { retryInterval := time.Second * 2 @@ -126,9 +139,29 @@ func (t *txSubmitterImpl) GetDistinctInterval() time.Duration { return retryInterval } +// attemptMarkStarted attempts to mark the submitter as started. +// if the submitter is already started, an error is returned. +func (t *txSubmitterImpl) attemptMarkStarted() error { + t.startMux.Lock() + defer t.startMux.Unlock() + if t.started { + return ErrSubmitterAlreadyStarted + } + t.started = true + return nil +} + +// ErrSubmitterAlreadyStarted is the error for when the submitter is already started. +var ErrSubmitterAlreadyStarted = errors.New("submitter already started") + // Start starts the transaction submitter. // nolint: cyclop func (t *txSubmitterImpl) Start(parentCtx context.Context) (err error) { + err = t.attemptMarkStarted() + if err != nil { + return err + } + t.otelRecorder, err = newOtelRecorder(t.metrics, t.signer) if err != nil { return fmt.Errorf("could not create otel recorder: %w", err) @@ -313,6 +346,9 @@ func (t *txSubmitterImpl) triggerProcessQueue(ctx context.Context) { } } +// ErrNotStarted is the error for when the submitter is not started. +var ErrNotStarted = errors.New("submitter is not started") + // nolint: cyclop func (t *txSubmitterImpl) SubmitTransaction(parentCtx context.Context, chainID *big.Int, call ContractCallType) (nonce uint64, err error) { ctx, span := t.metrics.Tracer().Start(parentCtx, "submitter.SubmitTransaction", trace.WithAttributes( @@ -324,6 +360,10 @@ func (t *txSubmitterImpl) SubmitTransaction(parentCtx context.Context, chainID * metrics.EndSpanWithErr(span, err) }() + if !t.Started() { + return 0, ErrNotStarted + } + // make sure we have a client for this chain. chainClient, err := t.fetcher.GetClient(ctx, chainID) if err != nil { diff --git a/services/rfq/contracts/fastbridge/events.go b/services/rfq/contracts/fastbridge/events.go index 9ebbc6c5d6..d1e9bef16e 100644 --- a/services/rfq/contracts/fastbridge/events.go +++ b/services/rfq/contracts/fastbridge/events.go @@ -17,6 +17,8 @@ var ( BridgeProofProvidedTopic common.Hash // BridgeDepositClaimedTopic is the topic emitted by a bridge relay. BridgeDepositClaimedTopic common.Hash + // BridgeProofDisputedTopic is the topic emitted by a bridge dispute. + BridgeProofDisputedTopic common.Hash ) // static checks to make sure topics actually exist. @@ -32,6 +34,7 @@ func init() { BridgeRelayedTopic = parsedABI.Events["BridgeRelayed"].ID BridgeProofProvidedTopic = parsedABI.Events["BridgeProofProvided"].ID BridgeDepositClaimedTopic = parsedABI.Events["BridgeDepositClaimed"].ID + BridgeProofDisputedTopic = parsedABI.Events["BridgeProofDisputed"].ID _, err = parsedABI.EventByID(BridgeRequestedTopic) if err != nil { @@ -47,6 +50,11 @@ func init() { if err != nil { panic(err) } + + _, err = parsedABI.EventByID(BridgeProofDisputedTopic) + if err != nil { + panic(err) + } } // topicMap maps events to topics. @@ -57,6 +65,7 @@ func topicMap() map[EventType]common.Hash { BridgeRelayedEvent: BridgeRelayedTopic, BridgeProofProvidedEvent: BridgeProofProvidedTopic, BridgeDepositClaimedEvent: BridgeDepositClaimedTopic, + BridgeDisputeEvent: BridgeProofDisputedTopic, } } diff --git a/services/rfq/contracts/fastbridge/eventtype_string.go b/services/rfq/contracts/fastbridge/eventtype_string.go index 35a39eddf4..10e419b234 100644 --- a/services/rfq/contracts/fastbridge/eventtype_string.go +++ b/services/rfq/contracts/fastbridge/eventtype_string.go @@ -12,11 +12,12 @@ func _() { _ = x[BridgeRelayedEvent-2] _ = x[BridgeProofProvidedEvent-3] _ = x[BridgeDepositClaimedEvent-4] + _ = x[BridgeDisputeEvent-5] } -const _EventType_name = "BridgeRequestedEventBridgeRelayedEventBridgeProofProvidedEventBridgeDepositClaimedEvent" +const _EventType_name = "BridgeRequestedEventBridgeRelayedEventBridgeProofProvidedEventBridgeDepositClaimedEventBridgeDisputeEvent" -var _EventType_index = [...]uint8{0, 20, 38, 62, 87} +var _EventType_index = [...]uint8{0, 20, 38, 62, 87, 105} func (i EventType) String() string { i -= 1 diff --git a/services/rfq/contracts/fastbridge/parser.go b/services/rfq/contracts/fastbridge/parser.go index 19bbe73024..29faa55352 100644 --- a/services/rfq/contracts/fastbridge/parser.go +++ b/services/rfq/contracts/fastbridge/parser.go @@ -20,6 +20,8 @@ const ( BridgeProofProvidedEvent // BridgeDepositClaimedEvent is the event type for the BridgeDepositClaimed event. BridgeDepositClaimedEvent + // BridgeDisputeEvent is the event type for the BridgeDispute event. + BridgeDisputeEvent ) // Parser parses events from the fastbridge contracat. @@ -82,6 +84,13 @@ func (p parserImpl) ParseEvent(log ethTypes.Log) (_ EventType, event interface{} return noOpEvent, nil, false } return eventType, claimed, true + case BridgeDisputeEvent: + disputed, err := p.filterer.ParseBridgeProofDisputed(log) + if err != nil { + return noOpEvent, nil, false + } + return eventType, disputed, true + } return eventType, nil, true diff --git a/services/rfq/guard/service/guard.go b/services/rfq/guard/service/guard.go index 33eaa5727f..eaf2e71823 100644 --- a/services/rfq/guard/service/guard.go +++ b/services/rfq/guard/service/guard.go @@ -2,6 +2,7 @@ package service import ( "context" + "errors" "fmt" "time" @@ -124,6 +125,17 @@ func (g *Guard) Start(ctx context.Context) (err error) { return nil }) + group.Go(func() error { + if !g.txSubmitter.Started() { + err = g.txSubmitter.Start(ctx) + // defensive coding against potential race. + if err != nil && !errors.Is(err, submitter.ErrSubmitterAlreadyStarted) { + return fmt.Errorf("could not start tx submitter: %w", err) + } + } + return nil + }) + err = group.Wait() if err != nil { return fmt.Errorf("could not wait for group: %w", err) diff --git a/services/rfq/guard/service/handlers.go b/services/rfq/guard/service/handlers.go index 26c2b9dd52..cc98c7e4e7 100644 --- a/services/rfq/guard/service/handlers.go +++ b/services/rfq/guard/service/handlers.go @@ -135,11 +135,11 @@ func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.Pen } } else { // trigger dispute - contract, ok := g.contracts[int(bridgeRequest.Transaction.DestChainId)] + contract, ok := g.contracts[int(bridgeRequest.Transaction.OriginChainId)] if !ok { - return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.DestChainId) + return fmt.Errorf("could not get contract for chain: %d", bridgeRequest.Transaction.OriginChainId) } - _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(proven.Origin)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { + _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(bridgeRequest.Transaction.OriginChainId)), func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) { tx, err = contract.Dispute(transactor, proven.TransactionID) if err != nil { return nil, fmt.Errorf("could not dispute: %w", err) @@ -147,6 +147,7 @@ func (g *Guard) handleProveCalled(parentCtx context.Context, proven *guarddb.Pen return tx, nil }) + if err != nil { return fmt.Errorf("could not dispute: %w", err) } From 7ceb0dbcde0d81008ee19469b72247a8af49d46a Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 6 Jul 2024 01:02:58 -0400 Subject: [PATCH 35/37] fix tests --- ethergo/backends/anvil/anvil.go | 3 ++- ethergo/submitter/submitter.go | 2 +- services/rfq/e2e/setup_test.go | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ethergo/backends/anvil/anvil.go b/ethergo/backends/anvil/anvil.go index 9c95d15da9..c5c88410af 100644 --- a/ethergo/backends/anvil/anvil.go +++ b/ethergo/backends/anvil/anvil.go @@ -80,7 +80,8 @@ func NewAnvilBackend(ctx context.Context, t *testing.T, args *OptionBuilder) *Ba runOptions := &dockertest.RunOptions{ Repository: "ghcr.io/foundry-rs/foundry", - Tag: "nightly-deb3116955eea4333f9e4e4516104be4182e9ee2", + Tag: "nightly-1bac1b3d79243cea755800bf396c30a3d74741bf", + Platform: "linux/amd64", Cmd: []string{strings.Join(append([]string{"anvil"}, commandArgs...), " ")}, Labels: map[string]string{ "test-id": uuid.New().String(), diff --git a/ethergo/submitter/submitter.go b/ethergo/submitter/submitter.go index b79dfecb32..2d0a781688 100644 --- a/ethergo/submitter/submitter.go +++ b/ethergo/submitter/submitter.go @@ -361,7 +361,7 @@ func (t *txSubmitterImpl) SubmitTransaction(parentCtx context.Context, chainID * }() if !t.Started() { - return 0, ErrNotStarted + logger.Errorf("%v in a future version, this will hard error", ErrNotStarted.Error()) } // make sure we have a client for this chain. diff --git a/services/rfq/e2e/setup_test.go b/services/rfq/e2e/setup_test.go index bed24ccc7f..460047ffc8 100644 --- a/services/rfq/e2e/setup_test.go +++ b/services/rfq/e2e/setup_test.go @@ -21,7 +21,6 @@ import ( "github.com/synapsecns/sanguine/ethergo/backends" "github.com/synapsecns/sanguine/ethergo/backends/anvil" "github.com/synapsecns/sanguine/ethergo/backends/base" - "github.com/synapsecns/sanguine/ethergo/backends/geth" "github.com/synapsecns/sanguine/ethergo/contracts" signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" "github.com/synapsecns/sanguine/ethergo/signer/wallet" @@ -114,7 +113,9 @@ func (i *IntegrationSuite) setupBackends() { }() go func() { defer wg.Done() - i.destBackend = geth.NewEmbeddedBackendForChainID(i.GetTestContext(), i.T(), big.NewInt(destBackendChainID)) + options := anvil.NewAnvilOptionBuilder() + options.SetChainID(destBackendChainID) + i.destBackend = anvil.NewAnvilBackend(i.GetTestContext(), i.T(), options) i.setupBE(i.destBackend) }() wg.Wait() From 71133e6625cac59cd5a373b7aad1cb61ba7ebf89 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 6 Jul 2024 01:03:30 -0400 Subject: [PATCH 36/37] anvil removal [goreleaser] --- services/rfq/e2e/rfq_test.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 3275c9a1c7..48108d8fbe 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -103,10 +103,6 @@ func (i *IntegrationSuite) getOtherBackend(backend backends.SimulatedTestBackend } func (i *IntegrationSuite) TestUSDCtoUSDC() { - if core.GetEnvBool("CI", false) { - i.T().Skip("skipping until anvil issues are fixed in CI") - } - // start the relayer and guard go func() { _ = i.relayer.Start(i.GetTestContext()) @@ -266,9 +262,6 @@ func (i *IntegrationSuite) TestUSDCtoUSDC() { // nolint: cyclop func (i *IntegrationSuite) TestETHtoETH() { - if core.GetEnvBool("CI", false) { - i.T().Skip("skipping until anvil issues are fixed in CI") - } // start the relayer and guard go func() { @@ -388,10 +381,6 @@ func (i *IntegrationSuite) TestETHtoETH() { } func (i *IntegrationSuite) TestDispute() { - if core.GetEnvBool("CI", false) { - i.T().Skip("skipping until anvil issues are fixed in CI") - } - // start the guard go func() { _ = i.guard.Start(i.GetTestContext()) From 63e553c596708e025e6df9ff3fb6865f43d8d550 Mon Sep 17 00:00:00 2001 From: Trajan0x Date: Sat, 6 Jul 2024 01:07:44 -0400 Subject: [PATCH 37/37] ci again --- ethergo/backends/anvil/anvil.go | 3 ++- services/rfq/e2e/rfq_test.go | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ethergo/backends/anvil/anvil.go b/ethergo/backends/anvil/anvil.go index c5c88410af..2f631bedec 100644 --- a/ethergo/backends/anvil/anvil.go +++ b/ethergo/backends/anvil/anvil.go @@ -82,7 +82,8 @@ func NewAnvilBackend(ctx context.Context, t *testing.T, args *OptionBuilder) *Ba Repository: "ghcr.io/foundry-rs/foundry", Tag: "nightly-1bac1b3d79243cea755800bf396c30a3d74741bf", Platform: "linux/amd64", - Cmd: []string{strings.Join(append([]string{"anvil"}, commandArgs...), " ")}, + + Cmd: []string{strings.Join(append([]string{"anvil"}, commandArgs...), " ")}, Labels: map[string]string{ "test-id": uuid.New().String(), }, diff --git a/services/rfq/e2e/rfq_test.go b/services/rfq/e2e/rfq_test.go index 48108d8fbe..f75f3a9586 100644 --- a/services/rfq/e2e/rfq_test.go +++ b/services/rfq/e2e/rfq_test.go @@ -73,11 +73,6 @@ const ( func (i *IntegrationSuite) SetupTest() { i.TestSuite.SetupTest() - // TODO: no need for this when anvil CI issues are fixed - if core.GetEnvBool("CI", false) { - return - } - i.manager = testutil.NewDeployManager(i.T()) i.cctpDeployManager = cctpTest.NewDeployManager(i.T()) // TODO: consider jaeger