diff --git a/agents/agents/guard/db/agent_root_test.go b/agents/agents/guard/db/agent_root_test.go index 1c7ad56da5..68b245d7a5 100644 --- a/agents/agents/guard/db/agent_root_test.go +++ b/agents/agents/guard/db/agent_root_test.go @@ -33,11 +33,11 @@ func (t *DBSuite) TestGetSummitBlockNumberForRoot() { Nil(t.T(), err) // Call GetSummitBlockNumberForRoot for each agent root. - blockNumber, err := testDB.GetSummitBlockNumberForRoot(t.GetTestContext(), agentRootA) + blockNumber, err := testDB.GetSummitBlockNumberForRoot(t.GetTestContext(), agentRootA.String()) Nil(t.T(), err) Equal(t.T(), blockNumberA, blockNumber) - blockNumber, err = testDB.GetSummitBlockNumberForRoot(t.GetTestContext(), agentRootB) + blockNumber, err = testDB.GetSummitBlockNumberForRoot(t.GetTestContext(), agentRootB.String()) Nil(t.T(), err) Equal(t.T(), blockNumberB, blockNumber) }) diff --git a/agents/agents/guard/db/crosstable_test.go b/agents/agents/guard/db/crosstable_test.go index ed8fee1cab..1fb9e53411 100644 --- a/agents/agents/guard/db/crosstable_test.go +++ b/agents/agents/guard/db/crosstable_test.go @@ -10,10 +10,8 @@ import ( "github.com/synapsecns/sanguine/agents/types" ) -func (t *DBSuite) TestGetUpdateAgentStatusParameters() { +func (t *DBSuite) TestGetRelayableAgentStatuses() { t.RunOnAllDBs(func(testDB db.GuardDB) { - guardAddress := common.BigToAddress(big.NewInt(gofakeit.Int64())) - addressA := common.BigToAddress(big.NewInt(gofakeit.Int64())) addressB := common.BigToAddress(big.NewInt(gofakeit.Int64())) addressC := common.BigToAddress(big.NewInt(gofakeit.Int64())) @@ -48,37 +46,36 @@ func (t *DBSuite) TestGetUpdateAgentStatusParameters() { ) Nil(t.T(), err) - // Insert three rows into `Dispute`, two will have matching agent address to `AgentTree` rows and with status `Resolved`. - err = testDB.StoreDispute( + // Insert three rows into `RelayableAgentStatus`, two will have matching agent address to `AgentTree` rows and with status `Queued`. + chainA := gofakeit.Uint32() + chainB := chainA + 1 + err = testDB.StoreRelayableAgentStatus( t.GetTestContext(), - big.NewInt(gofakeit.Int64()), - types.Resolved, - guardAddress, - gofakeit.Uint32(), addressA, + types.AgentFlagUnknown, + types.AgentFlagActive, + chainA, ) Nil(t.T(), err) - err = testDB.StoreDispute( + err = testDB.StoreRelayableAgentStatus( t.GetTestContext(), - big.NewInt(gofakeit.Int64()), - types.Resolved, - guardAddress, - gofakeit.Uint32(), addressB, + types.AgentFlagUnknown, + types.AgentFlagActive, + chainA, ) Nil(t.T(), err) - err = testDB.StoreDispute( + err = testDB.StoreRelayableAgentStatus( t.GetTestContext(), - big.NewInt(gofakeit.Int64()), - types.Opened, - guardAddress, - gofakeit.Uint32(), addressC, + types.AgentFlagUnknown, + types.AgentFlagActive, + chainB, ) Nil(t.T(), err) // Get the matching agent tree from the database. - agentTrees, err := testDB.GetUpdateAgentStatusParameters(t.GetTestContext()) + agentTrees, err := testDB.GetRelayableAgentStatuses(t.GetTestContext(), chainA) Nil(t.T(), err) Equal(t.T(), 2, len(agentTrees)) diff --git a/agents/agents/guard/db/guard_db.go b/agents/agents/guard/db/guard_db.go index 7ebf5f9aa9..ffcf6a3d2e 100644 --- a/agents/agents/guard/db/guard_db.go +++ b/agents/agents/guard/db/guard_db.go @@ -2,7 +2,6 @@ package db import ( "context" - "math/big" "github.com/ethereum/go-ethereum/common" agentTypes "github.com/synapsecns/sanguine/agents/types" @@ -11,22 +10,20 @@ import ( // GuardDBWriter is the interface for writing to the guard's database. type GuardDBWriter interface { - // StoreDispute stores a dispute. - StoreDispute( + // StoreRelayableAgentStatus stores a relayable agent status. + StoreRelayableAgentStatus( ctx context.Context, - disputeIndex *big.Int, - disputeProcessedStatus agentTypes.DisputeProcessedStatus, - guardAddress common.Address, - notaryIndex uint32, - notaryAddress common.Address, + agentAddress common.Address, + staleFlag agentTypes.AgentFlagType, + updatedFlag agentTypes.AgentFlagType, + domain uint32, ) error - // UpdateDisputeProcessedStatus updates the DisputedProcessedStatus for a dispute. - UpdateDisputeProcessedStatus( + // UpdateAgentStatusRelayedState updates the relayed state for a relayable agent status. + UpdateAgentStatusRelayedState( ctx context.Context, - guardAddress *common.Address, - notaryAddress *common.Address, - flag agentTypes.DisputeProcessedStatus, + agentAddress common.Address, + state agentTypes.AgentStatusRelayedState, ) error // StoreAgentTree stores an agent tree. @@ -48,10 +45,10 @@ type GuardDBWriter interface { // GuardDBReader is the interface for reading from the guard's database. type GuardDBReader interface { - // GetUpdateAgentStatusParameters gets eligible parameters for the updateAgentStatus() contract call. - GetUpdateAgentStatusParameters(ctx context.Context) ([]agentTypes.AgentTree, error) + // GetRelayableAgentStatuses gets eligible parameters for the updateAgentStatus() contract call. + GetRelayableAgentStatuses(ctx context.Context, chainID uint32) ([]agentTypes.AgentTree, error) // GetSummitBlockNumberForRoot gets the summit block number for a given agent root. - GetSummitBlockNumberForRoot(ctx context.Context, agentRoot [32]byte) (uint64, error) + GetSummitBlockNumberForRoot(ctx context.Context, agentRoot string) (uint64, error) } // GuardDB is the interface for the guard's database. diff --git a/agents/agents/guard/db/sql/base/agent_root.go b/agents/agents/guard/db/sql/base/agent_root.go index 1e7539505e..d6756c883f 100644 --- a/agents/agents/guard/db/sql/base/agent_root.go +++ b/agents/agents/guard/db/sql/base/agent_root.go @@ -36,12 +36,11 @@ func (s Store) StoreAgentRoot( } // GetSummitBlockNumberForRoot gets the summit block number for a given agent root. -func (s Store) GetSummitBlockNumberForRoot(ctx context.Context, agentRoot [32]byte) (uint64, error) { - dbAgentRoot := common.BytesToHash(agentRoot[:]).String() - +func (s Store) GetSummitBlockNumberForRoot(ctx context.Context, agentRoot string) (uint64, error) { var blockNumber uint64 dbTx := s.DB().WithContext(ctx). - Where(fmt.Sprintf("%s = ?", AgentRootFieldName), dbAgentRoot). + Where(fmt.Sprintf("%s = ?", AgentRootFieldName), agentRoot). + Order(fmt.Sprintf("%s ASC", BlockNumberFieldName)). Limit(1). Model(&AgentRoot{}). Pluck(BlockNumberFieldName, &blockNumber) diff --git a/agents/agents/guard/db/sql/base/base.go b/agents/agents/guard/db/sql/base/base.go index cbf9d6dba5..3fe73e7f51 100644 --- a/agents/agents/guard/db/sql/base/base.go +++ b/agents/agents/guard/db/sql/base/base.go @@ -33,7 +33,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(allModels, &Dispute{}, &AgentTree{}, &AgentRoot{}) + allModels = append(allModels, &RelayableAgentStatus{}, &AgentTree{}, &AgentRoot{}) allModels = append(allModels, txdb.GetAllModels()...) return allModels } diff --git a/agents/agents/guard/db/sql/base/crosstable.go b/agents/agents/guard/db/sql/base/crosstable.go index 47ee904349..1a5396c272 100644 --- a/agents/agents/guard/db/sql/base/crosstable.go +++ b/agents/agents/guard/db/sql/base/crosstable.go @@ -11,42 +11,53 @@ import ( "github.com/synapsecns/sanguine/core/dbcommon" ) -// GetUpdateAgentStatusParameters gets the parameters for updating the agent status with the following steps: -// 1. Outer join the `AgentTree` table on the `Dispute` table on the `NotaryAddress` <-> `AgentAddress` fields. -// 2. Filter the rows where the `DisputeProcessedStatus` is `Resolved`. -// 3. Return each of remaining rows' `AgentRoot`, `AgentAddress`, and `Proof` fields. -func (s Store) GetUpdateAgentStatusParameters(ctx context.Context) ([]agentTypes.AgentTree, error) { +type agentTreeWithStatus struct { + AgentTree + AgentDomain uint32 + UpdatedAgentFlag agentTypes.AgentFlagType +} + +// GetRelayableAgentStatuses gets the parameters for updating the agent status with the following steps: +// 1. Load the `AgentTree` table. +// 2. Filter the rows where the `AgentStatusRelayedState` is `Queued`, and the `Domain` is the given `chainID`. +// 3. Outer join the `AgentTree` table on the `RelayableAgentStatus` table on the `AgentAddress` fields. +// 4. Return all fields in the `AgentTree` table as well as `UpdatedFlag` from the `RelayableAgentStatus` table. +func (s Store) GetRelayableAgentStatuses(ctx context.Context, chainID uint32) ([]agentTypes.AgentTree, error) { agentTreesTableName, err := dbcommon.GetModelName(s.DB(), &AgentTree{}) if err != nil { return nil, fmt.Errorf("failed to get agent trees table name: %w", err) } - disputesTableName, err := dbcommon.GetModelName(s.DB(), &Dispute{}) + relayableAgentStatusesTableName, err := dbcommon.GetModelName(s.DB(), &RelayableAgentStatus{}) if err != nil { - return nil, fmt.Errorf("failed to get disputes table name: %w", err) + return nil, fmt.Errorf("failed to get relayable agent statuses table name: %w", err) } query, err := interpol.WithMap( ` - SELECT * FROM {agentTreesTable} AS aTable + SELECT aTable.*, rTable.{updatedFlag} + FROM {agentTreesTable} AS aTable JOIN ( - SELECT * FROM {disputesTable} WHERE {disputeProcessedStatus} = ? - ) AS dTable - ON aTable.{agentAddress} = dTable.{notaryAddress} + SELECT * FROM {relayableAgentStatusesTable} + WHERE {agentStatusRelayedState} = ? + AND {domain} = ? + ) AS rTable + ON aTable.{agentAddress} = rTable.{agentAddress} `, map[string]string{ - "agentTreesTable": agentTreesTableName, - "disputesTable": disputesTableName, - "agentAddress": AgentAddressFieldName, - "notaryAddress": NotaryAddressFieldName, - "disputeProcessedStatus": DisputeProcessedStatusFieldName, + "domain": DomainFieldName, + "updatedFlag": UpdatedFlagFieldName, + "agentTreesTable": agentTreesTableName, + "relayableAgentStatusesTable": relayableAgentStatusesTableName, + "agentStatusRelayedState": AgentStatusRelayedStateFieldName, + "agentAddress": AgentAddressFieldName, }) if err != nil { return nil, fmt.Errorf("failed to interpolate query: %w", err) } - var dbAgentTrees []AgentTree - err = s.DB().WithContext(ctx).Raw(query, agentTypes.Resolved).Scan(&dbAgentTrees).Error + var dbAgentTrees []agentTreeWithStatus + err = s.DB().WithContext(ctx).Raw(query, agentTypes.Queued, chainID).Scan(&dbAgentTrees).Error if err != nil { return nil, fmt.Errorf("failed to get agent trees: %w", err) } @@ -60,10 +71,12 @@ func (s Store) GetUpdateAgentStatusParameters(ctx context.Context) ([]agentTypes return nil, fmt.Errorf("could not unmarshal proof: %w", err) } agentTrees = append(agentTrees, agentTypes.AgentTree{ - AgentRoot: tree.AgentRoot, - AgentAddress: common.HexToAddress(tree.AgentAddress), - BlockNumber: tree.BlockNumber, - Proof: proofBytes, + AgentRoot: tree.AgentRoot, + AgentAddress: common.HexToAddress(tree.AgentAddress), + AgentDomain: chainID, + UpdatedAgentFlag: tree.UpdatedAgentFlag, + BlockNumber: tree.BlockNumber, + Proof: proofBytes, }) } return agentTrees, nil diff --git a/agents/agents/guard/db/sql/base/dispute.go b/agents/agents/guard/db/sql/base/dispute.go deleted file mode 100644 index 1dab7343e0..0000000000 --- a/agents/agents/guard/db/sql/base/dispute.go +++ /dev/null @@ -1,71 +0,0 @@ -package base - -import ( - "context" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - agentTypes "github.com/synapsecns/sanguine/agents/types" - "gorm.io/gorm/clause" -) - -// StoreDispute stores an dispute. -func (s Store) StoreDispute( - ctx context.Context, - disputeIndex *big.Int, - disputeProcessedStatus agentTypes.DisputeProcessedStatus, - guardAddress common.Address, - notaryIndex uint32, - notaryAddress common.Address, -) error { - dbTx := s.DB().WithContext(ctx). - Clauses(clause.OnConflict{ - Columns: []clause.Column{ - {Name: DisputeIndexFieldName}, - }, - DoNothing: true, - }). - Create(&Dispute{ - DisputeIndex: disputeIndex.Uint64(), - DisputeProcessedStatus: disputeProcessedStatus, - GuardAddress: guardAddress.String(), - NotaryIndex: uint64(notaryIndex), - NotaryAddress: notaryAddress.String(), - }) - - if dbTx.Error != nil { - return fmt.Errorf("failed to store dispute: %w", dbTx.Error) - } - - return nil -} - -// UpdateDisputeProcessedStatus updates the disputed processed status for a dispute. -func (s Store) UpdateDisputeProcessedStatus( - ctx context.Context, - guardAddress *common.Address, - notaryAddress *common.Address, - flag agentTypes.DisputeProcessedStatus, -) error { - disputeMask := Dispute{ - DisputeProcessedStatus: agentTypes.Opened, - } - switch { - case guardAddress != nil: - disputeMask.GuardAddress = guardAddress.String() - case notaryAddress != nil: - disputeMask.NotaryAddress = notaryAddress.String() - default: - return fmt.Errorf("guardAddress or notaryAddress must be set") - } - - dbTx := s.DB().WithContext(ctx).Debug(). - Model(&Dispute{}). - Where(disputeMask). - Update(DisputeProcessedStatusFieldName, flag) - if dbTx.Error != nil { - return fmt.Errorf("failed to update dispute processed status %w", dbTx.Error) - } - return nil -} diff --git a/agents/agents/guard/db/sql/base/model.go b/agents/agents/guard/db/sql/base/model.go index 79759024f8..2f6454f2b2 100644 --- a/agents/agents/guard/db/sql/base/model.go +++ b/agents/agents/guard/db/sql/base/model.go @@ -13,40 +13,40 @@ import ( func init() { namer := dbcommon.NewNamer(GetAllModels()) AgentRootFieldName = namer.GetConsistentName("AgentRoot") - DisputeIndexFieldName = namer.GetConsistentName("DisputeIndex") AgentAddressFieldName = namer.GetConsistentName("AgentAddress") BlockNumberFieldName = namer.GetConsistentName("BlockNumber") - DisputeProcessedStatusFieldName = namer.GetConsistentName("DisputeProcessedStatus") - NotaryAddressFieldName = namer.GetConsistentName("NotaryAddress") + AgentStatusRelayedStateFieldName = namer.GetConsistentName("AgentStatusRelayedState") + DomainFieldName = namer.GetConsistentName("Domain") + UpdatedFlagFieldName = namer.GetConsistentName("UpdatedFlag") } var ( // AgentRootFieldName is the field name of the agent root. AgentRootFieldName string - // DisputeIndexFieldName is the field name of the agent root. - DisputeIndexFieldName string // AgentAddressFieldName gets the agent address field name. AgentAddressFieldName string // BlockNumberFieldName gets the agent block number field name. BlockNumberFieldName string - // DisputeProcessedStatusFieldName gets the dispute processed status field name. - DisputeProcessedStatusFieldName string - // NotaryAddressFieldName gets the notary address field name. - NotaryAddressFieldName string + // AgentStatusRelayedStateFieldName gets the relayable agent status field name. + AgentStatusRelayedStateFieldName string + // DomainFieldName gets the agent domain field name. + DomainFieldName string + // UpdatedFlagFieldName gets the updated flag field name. + UpdatedFlagFieldName string ) -// Dispute is a dispute between two agents. -type Dispute struct { - // DisputeIndex is the index of the dispute on the BondingManager. - DisputeIndex uint64 `gorm:"column:dispute_index;primaryKey"` - // DisputeProcessedStatus indicates the status of the dispute. - DisputeProcessedStatus agentTypes.DisputeProcessedStatus `gorm:"column:dispute_processed_status"` - // GuardAddress is the address of the guard. - GuardAddress string `gorm:"column:guard_address"` - // NotaryIndex is the index of the notary on the BondingManager. - NotaryIndex uint64 `gorm:"column:notary_index"` - // NotaryAddress is the address of the notary. - NotaryAddress string `gorm:"column:notary_address"` +// RelayableAgentStatus is used for tracking agent statuses that are out of +// sync and need to be relayed to a remote chain. +type RelayableAgentStatus struct { + AgentAddress string `gorm:"column:agent_address"` + // StaleFlag is the old flag that needs to be updated. + StaleFlag agentTypes.AgentFlagType `gorm:"column:stale_flag"` + // UpdatedFlag is the new flag value that should be relayed. + UpdatedFlag agentTypes.AgentFlagType `gorm:"column:updated_flag"` + // Domain is the domain of the agent status. + Domain uint32 `gorm:"column:domain"` + // AgentStatusRelayedState is the state of the relayable agent status. + AgentStatusRelayedState agentTypes.AgentStatusRelayedState `gorm:"column:agent_status_relayed_state"` } // AgentTree is the state of an agent tree on Summit. diff --git a/agents/agents/guard/db/sql/base/relayable_agent_status.go b/agents/agents/guard/db/sql/base/relayable_agent_status.go new file mode 100644 index 0000000000..a4267f4a63 --- /dev/null +++ b/agents/agents/guard/db/sql/base/relayable_agent_status.go @@ -0,0 +1,53 @@ +package base + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + agentTypes "github.com/synapsecns/sanguine/agents/types" +) + +// StoreRelayableAgentStatus stores a relayable agent status. +func (s Store) StoreRelayableAgentStatus( + ctx context.Context, + agentAddress common.Address, + staleFlag agentTypes.AgentFlagType, + updatedFlag agentTypes.AgentFlagType, + domain uint32, +) error { + dbTx := s.DB().WithContext(ctx). + Create(&RelayableAgentStatus{ + AgentAddress: agentAddress.String(), + StaleFlag: staleFlag, + UpdatedFlag: updatedFlag, + Domain: domain, + AgentStatusRelayedState: agentTypes.Queued, + }) + + if dbTx.Error != nil { + return fmt.Errorf("failed to store relayable agent status: %w", dbTx.Error) + } + + return nil +} + +// UpdateAgentStatusRelayedState updates the state for a RelayableAgentStatus. +func (s Store) UpdateAgentStatusRelayedState( + ctx context.Context, + agentAddress common.Address, + state agentTypes.AgentStatusRelayedState, +) error { + mask := RelayableAgentStatus{ + AgentAddress: agentAddress.String(), + } + + dbTx := s.DB().WithContext(ctx). + Model(&RelayableAgentStatus{}). + Where(mask). + Update(AgentStatusRelayedStateFieldName, state) + if dbTx.Error != nil { + return fmt.Errorf("failed to update relayed state: %w", dbTx.Error) + } + return nil +} diff --git a/agents/agents/guard/fraud.go b/agents/agents/guard/fraud.go index 7ee238f16d..5e0f6fc36f 100644 --- a/agents/agents/guard/fraud.go +++ b/agents/agents/guard/fraud.go @@ -3,7 +3,6 @@ package guard import ( "context" "fmt" - "math/big" "github.com/ethereum/go-ethereum/common" ethTypes "github.com/ethereum/go-ethereum/core/types" @@ -12,6 +11,8 @@ import ( // handleSnapshot checks a snapshot for invalid states. // If an invalid state is found, initiate slashing and submit a state report. +// +//nolint:cyclop func (g Guard) handleSnapshot(ctx context.Context, log ethTypes.Log) error { fraudSnapshot, err := g.inboxParser.ParseSnapshotAccepted(log) if err != nil { @@ -20,7 +21,7 @@ func (g Guard) handleSnapshot(ctx context.Context, log ethTypes.Log) error { // Verify each state in the snapshot. for stateIndex, state := range fraudSnapshot.Snapshot.States() { - isSlashable, err := g.isStateSlashable(ctx, state, fraudSnapshot.Agent) + isSlashable, err := g.isStateSlashable(ctx, state) if err != nil { return fmt.Errorf("could not handle state: %w", err) } @@ -49,11 +50,18 @@ func (g Guard) handleSnapshot(ctx context.Context, log ethTypes.Log) error { return nil } - // Submit the state report. + // Submit the state report to summit. srSignature, _, _, err := state.SignState(ctx, g.bondedSigner) if err != nil { return fmt.Errorf("could not sign state: %w", err) } + ok, err := g.prepareStateReport(ctx, fraudSnapshot.Agent, g.summitDomainID) + if err != nil { + return fmt.Errorf("could not prepare state report on summit: %w", err) + } + if !ok { + continue + } _, err = g.domains[g.summitDomainID].Inbox().SubmitStateReportWithSnapshot( ctx, g.unbondedSigner, @@ -63,7 +71,27 @@ func (g Guard) handleSnapshot(ctx context.Context, log ethTypes.Log) error { fraudSnapshot.Signature, ) if err != nil { - return fmt.Errorf("could not submit state report with snapshot: %w", err) + return fmt.Errorf("could not submit state report with snapshot to summit: %w", err) + } + + // Submit the state report to the remote chain. + ok, err = g.prepareStateReport(ctx, fraudSnapshot.Agent, fraudSnapshot.AgentDomain) + if err != nil { + return fmt.Errorf("could not prepare state report on summit: %w", err) + } + if !ok { + continue + } + _, err = g.domains[fraudSnapshot.AgentDomain].LightInbox().SubmitStateReportWithSnapshot( + ctx, + g.unbondedSigner, + int64(stateIndex), + srSignature, + fraudSnapshot.Payload, + fraudSnapshot.Signature, + ) + if err != nil { + return fmt.Errorf("could not submit state report with snapshot to agent domain %d: %w", fraudSnapshot.AgentDomain, err) } } @@ -86,7 +114,7 @@ func (g Guard) shouldSubmitStateReport(ctx context.Context, snapshot *types.Frau // isStateSlashable checks if a state is slashable, i.e. if the state is valid on the // Origin, and if the agent is in a slashable status. -func (g Guard) isStateSlashable(ctx context.Context, state types.State, agent common.Address) (bool, error) { +func (g Guard) isStateSlashable(ctx context.Context, state types.State) (bool, error) { statePayload, err := state.Encode() if err != nil { return false, fmt.Errorf("could not encode state: %w", err) @@ -100,16 +128,7 @@ func (g Guard) isStateSlashable(ctx context.Context, state types.State, agent co if err != nil { return false, fmt.Errorf("could not check validity of state: %w", err) } - if isValid { - return false, nil - } - - // Verify that the agent is in a slashable status. - agentStatus, err := g.domains[state.Origin()].LightManager().GetAgentStatus(ctx, agent) - if err != nil { - return false, fmt.Errorf("could not get agent status: %w", err) - } - return isAgentSlashable(agentStatus.Flag()), nil + return !isValid, nil } // handleAttestation checks whether an attestation is valid. @@ -133,6 +152,8 @@ func (g Guard) handleAttestation(ctx context.Context, log ethTypes.Log) error { // handleValidAttestation handles an attestation that is valid, but may // attest to a snapshot that contains an invalid state. +// +//nolint:cyclop func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *types.FraudAttestation) error { // Fetch the attested snapshot. snapshot, err := g.domains[g.summitDomainID].Summit().GetNotarySnapshot(ctx, fraudAttestation.Payload) @@ -140,14 +161,14 @@ func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *typ return fmt.Errorf("could not get snapshot: %w", err) } + snapPayload, err := snapshot.Encode() + if err != nil { + return fmt.Errorf("could not encode snapshot: %w", err) + } + // Verify each state in the snapshot. for stateIndex, state := range snapshot.States() { - snapPayload, err := snapshot.Encode() - if err != nil { - return fmt.Errorf("could not encode snapshot: %w", err) - } - - isSlashable, err := g.isStateSlashable(ctx, state, fraudAttestation.Notary) + isSlashable, err := g.isStateSlashable(ctx, state) if err != nil { return fmt.Errorf("could not check if state is slashable: %w", err) } @@ -168,11 +189,18 @@ func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *typ return fmt.Errorf("could not verify state with attestation: %w", err) } - // Submit the state report. + // Submit the state report on summit. srSignature, _, _, err := state.SignState(ctx, g.bondedSigner) if err != nil { return fmt.Errorf("could not sign state: %w", err) } + ok, err := g.prepareStateReport(ctx, fraudAttestation.Notary, g.summitDomainID) + if err != nil { + return fmt.Errorf("could not prepare state report on summit: %w", err) + } + if !ok { + continue + } _, err = g.domains[g.summitDomainID].Inbox().SubmitStateReportWithAttestation( ctx, g.unbondedSigner, @@ -183,12 +211,75 @@ func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *typ fraudAttestation.Signature, ) if err != nil { - return fmt.Errorf("could not submit state report with attestation: %w", err) + return fmt.Errorf("could not submit state report with attestation on summit: %w", err) + } + + // Submit the state report on the remote chain. + ok, err = g.prepareStateReport(ctx, fraudAttestation.Notary, fraudAttestation.AgentDomain) + if err != nil { + return fmt.Errorf("could not prepare state report on remote: %w", err) + } + if !ok { + continue + } + _, err = g.domains[fraudAttestation.AgentDomain].LightInbox().SubmitStateReportWithAttestation( + ctx, + g.unbondedSigner, + int64(stateIndex), + srSignature, + snapPayload, + fraudAttestation.Payload, + fraudAttestation.Signature, + ) + if err != nil { + return fmt.Errorf("could not submit state report with attestation on agent domain %d: %w", fraudAttestation.AgentDomain, err) } } return nil } +// prepareStateReport checks if the given agent is in a slashable status (Active or Unstaking), +// and relays the agent status from Summit to the given chain if necessary. +func (g Guard) prepareStateReport(ctx context.Context, agent common.Address, chainID uint32) (ok bool, err error) { + var agentStatus types.AgentStatus + if chainID == g.summitDomainID { + agentStatus, err = g.domains[chainID].BondingManager().GetAgentStatus(ctx, agent) + } else { + agentStatus, err = g.domains[chainID].LightManager().GetAgentStatus(ctx, agent) + } + if err != nil { + return false, fmt.Errorf("could not get agent status: %w", err) + } + + //nolint:exhaustive + switch agentStatus.Flag() { + case types.AgentFlagUnknown: + if chainID == g.summitDomainID { + return false, fmt.Errorf("cannot submit state report for Unknown agent on summit") + } + // Update the agent status to active using the last known root on remote chain. + err = g.guardDB.StoreRelayableAgentStatus( + ctx, + agent, + types.AgentFlagUnknown, + types.AgentFlagActive, + chainID, + ) + if err != nil { + return false, fmt.Errorf("could not store relayable agent status: %w", err) + } + err = g.updateAgentStatus(ctx, chainID) + if err != nil { + return false, err + } + return true, nil + case types.AgentFlagActive, types.AgentFlagUnstaking: + return true, nil + default: + return false, nil + } +} + // handleInvalidAttestation handles an invalid attestation by initiating slashing on summit, // then submitting an attestation fraud report on the accused agent's Domain. func (g Guard) handleInvalidAttestation(ctx context.Context, fraudAttestation *types.FraudAttestation) error { @@ -313,6 +404,7 @@ func (g Guard) handleStatusUpdated(ctx context.Context, log ethTypes.Log, chainI return fmt.Errorf("could not get proof: %w", err) } + var remoteStatus types.AgentStatus if chainID == g.summitDomainID { err = g.guardDB.StoreAgentTree( ctx, @@ -335,21 +427,27 @@ func (g Guard) handleStatusUpdated(ctx context.Context, log ethTypes.Log, chainI } } - // Mark the open dispute for this agent as Resolved. - var guardAddress, notaryAddress *common.Address - if statusUpdated.Domain == 0 { - guardAddress = &statusUpdated.Agent - } else { - notaryAddress = &statusUpdated.Agent - } - err = g.guardDB.UpdateDisputeProcessedStatus( - ctx, - guardAddress, - notaryAddress, - types.Resolved, - ) - if err != nil { - return fmt.Errorf("could not update dispute processed status: %w", err) + if statusUpdated.Domain != 0 { + // Fetch the current remote status and check whether the status is synced. + remoteStatus, err = g.domains[statusUpdated.Domain].LightManager().GetAgentStatus(ctx, statusUpdated.Agent) + if err != nil { + return fmt.Errorf("could not get agent status: %w", err) + } + if remoteStatus.Flag() == types.AgentFlagType(statusUpdated.Flag) { + return nil + } + + // If not synced, store a relayable agent status. + err = g.guardDB.StoreRelayableAgentStatus( + ctx, + statusUpdated.Agent, + remoteStatus.Flag(), + types.AgentFlagType(statusUpdated.Flag), + statusUpdated.Domain, + ) + if err != nil { + return fmt.Errorf("could not store relayable agent status: %w", err) + } } default: logger.Infof("Witnessed agent status updated, but not handling [status=%d, agent=%s]", statusUpdated.Flag, statusUpdated.Agent) @@ -358,67 +456,6 @@ func (g Guard) handleStatusUpdated(ctx context.Context, log ethTypes.Log, chainI return nil } -// handleDisputeOpened stores models related to a DisputeOpened event. -func (g Guard) handleDisputeOpened(ctx context.Context, log ethTypes.Log) error { - disputeOpened, err := g.parseDisputeOpened(log) - if err != nil { - return fmt.Errorf("could not parse dispute opened: %w", err) - } - - _, guardAddress, err := g.domains[g.summitDomainID].BondingManager().GetAgent(ctx, big.NewInt(int64(disputeOpened.guardIndex))) - if err != nil { - return fmt.Errorf("could not get agent: %w", err) - } - - _, notaryAddress, err := g.domains[g.summitDomainID].BondingManager().GetAgent(ctx, big.NewInt(int64(disputeOpened.notaryIndex))) - if err != nil { - return fmt.Errorf("could not get agent: %w", err) - } - - // Store the dispute in the database. - err = g.guardDB.StoreDispute( - ctx, - disputeOpened.disputeIndex, - types.Opened, - guardAddress, - disputeOpened.notaryIndex, - notaryAddress, - ) - if err != nil { - return fmt.Errorf("could not store dispute: %w", err) - } - - return nil -} - -// disputeOpened is a wrapper struct used to merge the -// lightmanager.DisputeOpened and bondingmangaer.DisputeOpened structs. -type disputeOpened struct { - disputeIndex *big.Int - guardIndex uint32 - notaryIndex uint32 -} - -func (g Guard) parseDisputeOpened(log ethTypes.Log) (*disputeOpened, error) { - disputeOpenedLight, err := g.lightManagerParser.ParseDisputeOpened(log) - if err == nil { - return &disputeOpened{ - disputeIndex: disputeOpenedLight.DisputeIndex, - guardIndex: disputeOpenedLight.GuardIndex, - notaryIndex: disputeOpenedLight.NotaryIndex, - }, nil - } - disputeOpenedBonding, err := g.bondingManagerParser.ParseDisputeOpened(log) - if err == nil { - return &disputeOpened{ - disputeIndex: disputeOpenedBonding.DisputeIndex, - guardIndex: disputeOpenedBonding.GuardIndex, - notaryIndex: disputeOpenedBonding.NotaryIndex, - }, nil - } - return nil, fmt.Errorf("could not parse dispute opened: %w", err) -} - // handleRootUpdated stores models related to a RootUpdated event. func (g Guard) handleRootUpdated(ctx context.Context, log ethTypes.Log, chainID uint32) error { if chainID == g.summitDomainID { @@ -457,8 +494,10 @@ func (g Guard) updateAgentStatuses(ctx context.Context) error { // updateAgentStatus updates the status for each agent with a pending agent tree model, // and open dispute on remote chain. +// +//nolint:cyclop func (g Guard) updateAgentStatus(ctx context.Context, chainID uint32) error { - eligibleAgentTrees, err := g.guardDB.GetUpdateAgentStatusParameters(ctx) + eligibleAgentTrees, err := g.guardDB.GetRelayableAgentStatuses(ctx, chainID) if err != nil { return fmt.Errorf("could not get update agent status parameters: %w", err) } @@ -472,14 +511,20 @@ func (g Guard) updateAgentStatus(ctx context.Context, chainID uint32) error { return fmt.Errorf("could not get agent root: %w", err) } - blockNumber, err := g.guardDB.GetSummitBlockNumberForRoot(ctx, localRoot) + localRootBlockNumber, err := g.guardDB.GetSummitBlockNumberForRoot(ctx, common.BytesToHash(localRoot[:]).String()) if err != nil { - return fmt.Errorf("could not get latest confirmed summit block number: %w", err) + return fmt.Errorf("could not get block number for local root: %w", err) } - // Filter the eligible agent roots by the given block number and call updateAgentStatus() + // Filter the eligible agent roots by the given block number and call updateAgentStatus(). for _, tree := range eligibleAgentTrees { - if tree.BlockNumber >= blockNumber { + // Get the first recorded summit block number for the tree agent root. + treeBlockNumber, err := g.guardDB.GetSummitBlockNumberForRoot(ctx, tree.AgentRoot) + if err != nil { + return fmt.Errorf("could not get block number for local root: %w", err) + } + if localRootBlockNumber >= treeBlockNumber { + // Fetch the agent status to be relayed from Summit. agentStatus, err := g.domains[g.summitDomainID].BondingManager().GetAgentStatus(ctx, tree.AgentAddress) if err != nil { return fmt.Errorf("could not get agent status: %w", err) @@ -487,7 +532,9 @@ func (g Guard) updateAgentStatus(ctx context.Context, chainID uint32) error { if agentStatus.Domain() != chainID { continue } - _, err = g.domains[chainID].LightManager().UpdateAgentStatus( + + // Update agent status on remote. + tx, err := g.domains[chainID].LightManager().UpdateAgentStatus( ctx, g.unbondedSigner, tree.AgentAddress, @@ -497,6 +544,17 @@ func (g Guard) updateAgentStatus(ctx context.Context, chainID uint32) error { if err != nil { return fmt.Errorf("could not update agent status: %w", err) } + logger.Infof("Updated agent status on chain %d for agent %s: %s [hash: %s]", chainID, tree.AgentAddress.String(), agentStatus.Flag().String(), tx.Hash()) + + // Mark the relayable status as Relayed. + err = g.guardDB.UpdateAgentStatusRelayedState( + ctx, + tree.AgentAddress, + types.Relayed, + ) + if err != nil { + return fmt.Errorf("could not update agent status relayed state: %w", err) + } } } diff --git a/agents/agents/guard/fraud_test.go b/agents/agents/guard/fraud_test.go index ea83f90cc2..d6dd98b518 100644 --- a/agents/agents/guard/fraud_test.go +++ b/agents/agents/guard/fraud_test.go @@ -15,11 +15,13 @@ import ( "github.com/synapsecns/sanguine/agents/agents/guard" "github.com/synapsecns/sanguine/agents/config" execConfig "github.com/synapsecns/sanguine/agents/config/executor" + "github.com/synapsecns/sanguine/agents/domains" "github.com/synapsecns/sanguine/agents/testutil/agentstestcontract" "github.com/synapsecns/sanguine/agents/types" "github.com/synapsecns/sanguine/ethergo/backends" "github.com/synapsecns/sanguine/ethergo/backends/anvil" signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" + "github.com/synapsecns/sanguine/ethergo/signer/signer" submitterConfig "github.com/synapsecns/sanguine/ethergo/submitter/config" omniClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/scribe/backend" @@ -95,6 +97,21 @@ func (g GuardSuite) bumpBackend(backend backends.SimulatedTestBackend, contract backend.WaitForConfirmation(g.GetTestContext(), bumpTx) } +func (g GuardSuite) updateAgentStatus(lightManager domains.LightManagerContract, bondedSigner, unbondedSigner signer.Signer) { + agentStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), bondedSigner.Address()) + Nil(g.T(), err) + agentProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), bondedSigner.Address()) + Nil(g.T(), err) + _, err = lightManager.UpdateAgentStatus( + g.GetTestContext(), + unbondedSigner, + bondedSigner.Address(), + agentStatus, + agentProof, + ) + Nil(g.T(), err) +} + // TODO: Add a test for exiting the report logic early when the snapshot submitter is a guard. func (g GuardSuite) TestFraudulentStateInSnapshot() { testDone := false @@ -144,22 +161,29 @@ func (g GuardSuite) TestFraudulentStateInSnapshot() { }() // Update the agent status on Origin. - notaryStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) + g.updateAgentStatus(g.OriginDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) + + // Verify that the agent is marked as Active + status, err := g.OriginDomainClient.LightManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) + Equal(g.T(), status.Flag(), types.AgentFlagActive) + Nil(g.T(), err) + + // Store agent trees and roots so that the agent status can be updated by the guard. + agentRoot, err := g.SummitDomainClient.BondingManager().GetAgentRoot(g.GetTestContext()) + Nil(g.T(), err) + blockNumber, err := g.SummitDomainClient.BlockNumber(g.GetTestContext()) Nil(g.T(), err) notaryProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.NotaryBondedSigner.Address()) Nil(g.T(), err) - _, err = g.OriginDomainClient.LightManager().UpdateAgentStatus( + err = g.GuardTestDB.StoreAgentTree( g.GetTestContext(), - g.NotaryUnbondedSigner, + agentRoot, g.NotaryBondedSigner.Address(), - notaryStatus, + uint64(blockNumber), notaryProof, ) Nil(g.T(), err) - - // Verify that the agent is marked as Active - status, err := g.OriginDomainClient.LightManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Equal(g.T(), status.Flag(), types.AgentFlagActive) + err = g.GuardTestDB.StoreAgentRoot(g.GetTestContext(), agentRoot, uint64(blockNumber)) Nil(g.T(), err) // Before submitting the attestation, ensure that there are no disputes opened. @@ -210,27 +234,18 @@ func (g GuardSuite) TestFraudulentStateInSnapshot() { return false }) - // TODO: Once we add updating agent statuses fully, uncomment this. - // Verify that the guard eventually marks the accused agent as Fraudulent on Summit - // g.Eventually(func() bool { - // status, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - // Nil(g.T(), err) - - // if status.Flag() == types.AgentFlagSlashed { - // return true - // } - - // g.bumpBackends() - // return false - // }) - // Verify that a report has been submitted by the Guard by checking that a Dispute is now open. g.Eventually(func() bool { err = g.SummitDomainClient.BondingManager().GetDispute(g.GetTestContext(), big.NewInt(0)) return err == nil }) - // TODO: Add a unit test for testing the case where multiple states are in the same snapshot to ensure they are - // handled correctly. + + // Verify that a state report was submitted on summit. + fraudulentState := fraudulentSnapshot.States()[0] + g.verifyStateReport(g.InboxOnSummit, 1, fraudulentState) + + // Verify that a state report was submitted on destination. + g.verifyStateReport(g.LightInboxOnDestination, 1, fraudulentState) } func (g GuardSuite) TestFraudulentAttestationOnDestination() { @@ -321,31 +336,8 @@ func (g GuardSuite) TestFraudulentAttestationOnDestination() { NotNil(g.T(), err) // Update the agent status of the Guard and Notary. - guardStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - guardProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.GuardUnbondedSigner, - g.GuardBondedSigner.Address(), - guardStatus, - guardProof, - ) - Nil(g.T(), err) - - notaryStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - notaryProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.GuardBondedSigner, g.GuardUnbondedSigner) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) // Submit the attestation tx, err := g.DestinationDomainClient.LightInbox().SubmitAttestation( @@ -447,39 +439,9 @@ func (g GuardSuite) TestReportFraudulentStateInAttestation() { NotNil(g.T(), err) // Update the agent status of the Guard and Notary. - guardStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - guardProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.GuardUnbondedSigner, - g.GuardBondedSigner.Address(), - guardStatus, - guardProof, - ) - Nil(g.T(), err) - - notaryStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - notaryProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) - _, err = g.OriginDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.GuardBondedSigner, g.GuardUnbondedSigner) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) + g.updateAgentStatus(g.OriginDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) // Submit the snapshot with a guard guardSnapshotSignature, encodedSnapshot, _, err := fraudulentSnapshot.SignSnapshot(g.GetTestContext(), g.GuardBondedSigner) @@ -525,31 +487,50 @@ func (g GuardSuite) TestReportFraudulentStateInAttestation() { return false }) - // Verify that a report has been submitted by the Guard by checking that a Dispute is now open. + // Verify that a dispute is now open on summit. g.Eventually(func() bool { err := g.SummitDomainClient.BondingManager().GetDispute(g.GetTestContext(), big.NewInt(0)) return err == nil }) - // TODO: uncomment the following case once manager messages can be executed. - // // Increase EVM time to allow agent status to be updated to Slashed on origin. - // anvilClient, err := anvil.Dial(g.GetTestContext(), g.TestBackendOrigin.RPCAddress()) - // Nil(g.T(), err) - // optimisticPeriodSeconds := 86400 - // err = anvilClient.IncreaseTime(g.GetTestContext(), int64(optimisticPeriodSeconds)) - // Nil(g.T(), err) + // Verify that a state report was submitted on summit. + g.verifyStateReport(g.InboxOnSummit, 1, fraudulentState) - // // Verify that the guard eventually marks the accused agent as Slashed. - // g.Eventually(func() bool { - // status, err := g.OriginDomainClient.LightManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - // Nil(g.T(), err) - // if status.Flag() == types.AgentFlagSlashed { - // return true - // } + // Verify that a state report was submitted on destination. + g.verifyStateReport(g.LightInboxOnDestination, 1, fraudulentState) +} - // g.bumpBackends() - // return false - // }) +type statementInboxContract interface { + GetReportsAmount(opts *bind.CallOpts) (*big.Int, error) + GetGuardReport(opts *bind.CallOpts, index *big.Int) (struct { + StatementPayload []byte + ReportSignature []byte + }, error) +} + +// Verify that a state report was submitted on the given contract. +// +//nolint:unparam +func (g GuardSuite) verifyStateReport(contract statementInboxContract, expectedNumReports int64, expectedState types.State) { + g.Eventually(func() bool { + numReports, err := contract.GetReportsAmount(&bind.CallOpts{Context: g.GetTestContext()}) + Nil(g.T(), err) + + if numReports.Int64() < expectedNumReports { + return false + } + if numReports.Int64() != expectedNumReports { + g.T().Fatalf("too many reports; expected %d, got %v", expectedNumReports, numReports.Int64()) + } + + stateReportIdx := big.NewInt(numReports.Int64() - 1) + stateReport, err := contract.GetGuardReport(&bind.CallOpts{Context: g.GetTestContext()}, stateReportIdx) + Nil(g.T(), err) + + expected, err := expectedState.Encode() + Nil(g.T(), err) + return Equal(g.T(), stateReport.StatementPayload, expected) + }) } func (g GuardSuite) TestInvalidReceipt() { @@ -657,18 +638,7 @@ func (g GuardSuite) TestInvalidReceipt() { // Build and sign a receipt snapshotRoot, _, err := snapshot.SnapshotRootAndProofs() Nil(g.T(), err) - notaryStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - notaryProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) messageHash, err := message.ToLeaf() Nil(g.T(), err) receipt := types.NewReceipt( @@ -801,14 +771,6 @@ func (g GuardSuite) TestUpdateAgentStatusOnRemote() { Chains: []scribeConfig.ChainConfig{originChainConfig, destinationChainConfig, summitChainConfig}, } - /* - Test plan: - - - - - call setAgentRoot() on remote to emit RootUpdated event - - updateAgentStatus() on remote (requires agent proof that includes a root emitted in RootUpdated event) - */ - // Start a new Guard. guard, scribeClient, err := g.getTestGuard(scribeConfig) Nil(g.T(), err) @@ -899,40 +861,12 @@ func (g GuardSuite) TestUpdateAgentStatusOnRemote() { err = g.DestinationDomainClient.LightManager().GetDispute(g.GetTestContext(), big.NewInt(0)) NotNil(g.T(), err) - // Update the agent status of the Guard and Notary. - guardStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - guardProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.GuardBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.GuardUnbondedSigner, - g.GuardBondedSigner.Address(), - guardStatus, - guardProof, - ) - Nil(g.T(), err) - - notaryStatus, err := g.SummitDomainClient.BondingManager().GetAgentStatus(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - notaryProof, err := g.SummitDomainClient.BondingManager().GetProof(g.GetTestContext(), g.NotaryBondedSigner.Address()) - Nil(g.T(), err) - _, err = g.DestinationDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) - _, err = g.OriginDomainClient.LightManager().UpdateAgentStatus( - g.GetTestContext(), - g.NotaryUnbondedSigner, - g.NotaryBondedSigner.Address(), - notaryStatus, - notaryProof, - ) - Nil(g.T(), err) + // Update the agent status of the Guard and Notaries. + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.GuardBondedSigner, g.GuardUnbondedSigner) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) + g.updateAgentStatus(g.OriginDomainClient.LightManager(), g.NotaryBondedSigner, g.NotaryUnbondedSigner) + g.updateAgentStatus(g.DestinationDomainClient.LightManager(), g.NotaryOnDestinationBondedSigner, g.NotaryOnDestinationUnbondedSigner) + g.updateAgentStatus(g.OriginDomainClient.LightManager(), g.NotaryOnDestinationBondedSigner, g.NotaryOnDestinationUnbondedSigner) // Submit the snapshot with a guard guardSnapshotSignature, encodedSnapshot, _, err := fraudulentSnapshot.SignSnapshot(g.GetTestContext(), g.GuardBondedSigner) @@ -1030,7 +964,7 @@ func (g GuardSuite) TestUpdateAgentStatusOnRemote() { g.bumpBackends() // Increase executor time so that the manager message may be executed. - updatedTime := time.Now().Add(time.Duration(optimisticPeriodSeconds) * time.Second) + updatedTime := time.Now().Add(time.Duration(optimisticPeriodSeconds+10) * time.Second) currentTime = &updatedTime // Verify that the accused agent is eventually Slashed on Summit. @@ -1107,7 +1041,7 @@ func (g GuardSuite) TestUpdateAgentStatusOnRemote() { Nil(g.T(), err) // Submit the attestation. - attSignature, attEncoded, _, err = attestation.SignAttestation(g.GetTestContext(), g.NotaryBondedSigner, true) + attSignature, attEncoded, _, err = attestation.SignAttestation(g.GetTestContext(), g.NotaryOnDestinationBondedSigner, true) Nil(g.T(), err) tx, err = g.DestinationDomainClient.LightInbox().SubmitAttestation( txContextDest.TransactOpts, diff --git a/agents/agents/guard/guard.go b/agents/agents/guard/guard.go index a98166ac3c..141c86f328 100644 --- a/agents/agents/guard/guard.go +++ b/agents/agents/guard/guard.go @@ -255,8 +255,6 @@ func (g Guard) handleLog(ctx context.Context, log ethTypes.Log, chainID uint32) return g.handleAttestation(ctx, log) case isReceiptAcceptedEvent(g.inboxParser, log): return g.handleReceipt(ctx, log) - case isDisputeOpenedEvent(g.lightManagerParser, g.bondingManagerParser, log): - return g.handleDisputeOpened(ctx, log) case isStatusUpdatedEvent(g.bondingManagerParser, log): return g.handleStatusUpdated(ctx, log, chainID) case isRootUpdatedEvent(g.bondingManagerParser, log): diff --git a/agents/agents/guard/utils.go b/agents/agents/guard/utils.go index dc1b34e6a2..2a4a6ba0d6 100644 --- a/agents/agents/guard/utils.go +++ b/agents/agents/guard/utils.go @@ -5,8 +5,6 @@ import ( "github.com/synapsecns/sanguine/agents/contracts/bondingmanager" "github.com/synapsecns/sanguine/agents/contracts/inbox" "github.com/synapsecns/sanguine/agents/contracts/lightinbox" - "github.com/synapsecns/sanguine/agents/contracts/lightmanager" - "github.com/synapsecns/sanguine/agents/types" ) func isSnapshotAcceptedEvent(parser inbox.Parser, log ethTypes.Log) bool { @@ -24,18 +22,6 @@ func isReceiptAcceptedEvent(parser inbox.Parser, log ethTypes.Log) bool { return ok && inboxEvent == inbox.ReceiptAcceptedEvent } -func isDisputeOpenedEvent(lightParser lightmanager.Parser, bondingParser bondingmanager.Parser, log ethTypes.Log) bool { - lightManagerEvent, ok := lightParser.EventType(log) - if ok && lightManagerEvent == lightmanager.DisputeOpenedEvent { - return true - } - bondingManagerEvent, ok := bondingParser.EventType(log) - if ok && bondingManagerEvent == bondingmanager.DisputeOpenedEvent { - return true - } - return false -} - func isStatusUpdatedEvent(parser bondingmanager.Parser, log ethTypes.Log) bool { bondingManagerEvent, ok := parser.EventType(log) return ok && bondingManagerEvent == bondingmanager.StatusUpdatedEvent @@ -48,7 +34,3 @@ func isRootUpdatedEvent(bondingParser bondingmanager.Parser, log ethTypes.Log) b } return false } - -func isAgentSlashable(agentFlag types.AgentFlagType) bool { - return agentFlag == types.AgentFlagActive || agentFlag == types.AgentFlagUnstaking -} diff --git a/agents/domains/domain.go b/agents/domains/domain.go index 0fe46fcaf8..2843a51d82 100644 --- a/agents/domains/domain.go +++ b/agents/domains/domain.go @@ -143,6 +143,8 @@ type LightInboxContract interface { agentRoot [32]byte, snapGas []*big.Int, ) (tx *ethTypes.Transaction, err error) + // SubmitStateReportWithAttestation submits a state report corresponding to an attesation for an invalid state. + SubmitStateReportWithAttestation(ctx context.Context, signer signer.Signer, stateIndex int64, signature signer.Signature, snapPayload, attPayload, attSignature []byte) (tx *ethTypes.Transaction, err error) // VerifyStateWithSnapshot verifies a state within a snapshot. VerifyStateWithSnapshot(ctx context.Context, signer signer.Signer, stateIndex int64, snapPayload []byte, snapSignature []byte) (tx *ethTypes.Transaction, err error) // SubmitAttestationReport submits an attestation report to the inbox (via the light inbox contract) diff --git a/agents/domains/evm/inbox.go b/agents/domains/evm/inbox.go index cd05c5dc12..cd90815bc3 100644 --- a/agents/domains/evm/inbox.go +++ b/agents/domains/evm/inbox.go @@ -142,6 +142,7 @@ func (a inboxContract) VerifyAttestation(ctx context.Context, signer signer.Sign transactOpts.Context = ctx transactOpts.GasLimit = 5000000 + a.nonceManager.ClearNonce(signer.Address()) return a.contract.VerifyAttestation(transactOpts, attestation, attSignature) } @@ -162,6 +163,7 @@ func (a inboxContract) VerifyStateWithAttestation(ctx context.Context, signer si return a.contract.VerifyStateWithAttestation(transactOpts, big.NewInt(stateIndex), snapPayload, attPayload, attSignature) } +//nolint:dupl func (a inboxContract) SubmitStateReportWithAttestation(ctx context.Context, signer signer.Signer, stateIndex int64, signature signer.Signature, snapPayload, attPayload, attSignature []byte) (tx *ethTypes.Transaction, err error) { transactor, err := signer.GetTransactor(ctx, a.client.GetBigChainID()) if err != nil { @@ -174,8 +176,8 @@ func (a inboxContract) SubmitStateReportWithAttestation(ctx context.Context, sig } transactOpts.Context = ctx - transactOpts.GasLimit = 5000000 + a.nonceManager.ClearNonce(signer.Address()) rawSig, err := types.EncodeSignature(signature) if err != nil { diff --git a/agents/domains/evm/lightinbox.go b/agents/domains/evm/lightinbox.go index 674abc1d37..787d6fb5da 100644 --- a/agents/domains/evm/lightinbox.go +++ b/agents/domains/evm/lightinbox.go @@ -187,3 +187,37 @@ func (a lightInboxContract) VerifyReceipt(ctx context.Context, signer signer.Sig transactOpts.GasLimit = 5000000 return a.contract.VerifyReceipt(transactOpts, rcptPayload, rcptSignature) } + +//nolint:dupl +func (a lightInboxContract) SubmitStateReportWithAttestation(ctx context.Context, signer signer.Signer, stateIndex int64, signature signer.Signature, snapPayload, attPayload, attSignature []byte) (tx *ethTypes.Transaction, err error) { + transactor, err := signer.GetTransactor(ctx, a.client.GetBigChainID()) + if err != nil { + return nil, fmt.Errorf("could not sign tx: %w", err) + } + + transactOpts, err := a.nonceManager.NewKeyedTransactor(transactor) + if err != nil { + return nil, fmt.Errorf("could not create tx: %w", err) + } + + transactOpts.Context = ctx + transactOpts.GasLimit = 5000000 + a.nonceManager.ClearNonce(signer.Address()) + + rawSig, err := types.EncodeSignature(signature) + if err != nil { + return nil, fmt.Errorf("could not encode signature: %w", err) + } + + // TODO: Is there a way to get a return value from a contractTransactor call? + tx, err = a.contract.SubmitStateReportWithAttestation(transactOpts, big.NewInt(stateIndex), rawSig, snapPayload, attPayload, attSignature) + if err != nil { + // TODO: Why is this done? And if it is necessary, we should functionalize it. + if strings.Contains(err.Error(), "nonce too low") { + a.nonceManager.ClearNonce(signer.Address()) + } + return nil, fmt.Errorf("could not submit state report: %w", err) + } + + return tx, nil +} diff --git a/agents/testutil/simulated_backends_suite.go b/agents/testutil/simulated_backends_suite.go index 79f3d97216..fd31d2e612 100644 --- a/agents/testutil/simulated_backends_suite.go +++ b/agents/testutil/simulated_backends_suite.go @@ -1,11 +1,12 @@ package testutil import ( - "github.com/synapsecns/sanguine/core" "math/big" "sync" "testing" + "github.com/synapsecns/sanguine/core" + "github.com/Flaque/filet" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" @@ -101,14 +102,18 @@ type SimulatedBackendsTestSuite struct { TestBackendSummit backends.SimulatedTestBackend NotaryBondedWallet wallet.Wallet NotaryOnOriginBondedWallet wallet.Wallet + NotaryOnDestinationBondedWallet wallet.Wallet GuardBondedWallet wallet.Wallet NotaryBondedSigner signer.Signer NotaryOnOriginBondedSigner signer.Signer + NotaryOnDestinationBondedSigner signer.Signer GuardBondedSigner signer.Signer NotaryUnbondedWallet wallet.Wallet NotaryUnbondedSigner signer.Signer NotaryOnOriginUnbondedWallet wallet.Wallet NotaryOnOriginUnbondedSigner signer.Signer + NotaryOnDestinationUnbondedWallet wallet.Wallet + NotaryOnDestinationUnbondedSigner signer.Signer GuardUnbondedWallet wallet.Wallet GuardUnbondedSigner signer.Signer ExecutorUnbondedWallet wallet.Wallet @@ -199,6 +204,7 @@ func (a *SimulatedBackendsTestSuite) SetupOrigin(deployManager *DeployManager) { a.TestBackendOrigin.FundAccount(a.GetTestContext(), a.NotaryUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendOrigin.FundAccount(a.GetTestContext(), a.NotaryOnOriginUnbondedSigner.Address(), *big.NewInt(params.Ether)) + a.TestBackendOrigin.FundAccount(a.GetTestContext(), a.NotaryOnDestinationUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendOrigin.FundAccount(a.GetTestContext(), a.GuardUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendOrigin.FundAccount(a.GetTestContext(), a.ExecutorUnbondedSigner.Address(), *big.NewInt(params.Ether)) } @@ -239,6 +245,7 @@ func (a *SimulatedBackendsTestSuite) SetupDestination(deployManager *DeployManag a.TestBackendDestination.FundAccount(a.GetTestContext(), a.NotaryUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendDestination.FundAccount(a.GetTestContext(), a.NotaryOnOriginUnbondedSigner.Address(), *big.NewInt(params.Ether)) + a.TestBackendDestination.FundAccount(a.GetTestContext(), a.NotaryOnDestinationUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendDestination.FundAccount(a.GetTestContext(), a.GuardUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendDestination.FundAccount(a.GetTestContext(), a.ExecutorUnbondedSigner.Address(), *big.NewInt(params.Ether)) } @@ -266,6 +273,7 @@ func (a *SimulatedBackendsTestSuite) SetupSummit(deployManager *DeployManager) { a.TestBackendSummit.FundAccount(a.GetTestContext(), a.NotaryUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendSummit.FundAccount(a.GetTestContext(), a.NotaryOnOriginUnbondedSigner.Address(), *big.NewInt(params.Ether)) + a.TestBackendSummit.FundAccount(a.GetTestContext(), a.NotaryOnDestinationUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendSummit.FundAccount(a.GetTestContext(), a.GuardUnbondedSigner.Address(), *big.NewInt(params.Ether)) a.TestBackendSummit.FundAccount(a.GetTestContext(), a.ExecutorUnbondedSigner.Address(), *big.NewInt(params.Ether)) } @@ -318,6 +326,22 @@ func (a *SimulatedBackendsTestSuite) SetupNotaryOnOrigin() { a.NotaryOnOriginUnbondedSigner = localsigner.NewSigner(a.NotaryOnOriginUnbondedWallet.PrivateKey()) } +// SetupNotaryOnDestination sets up the Notary agent on the origin chain. +func (a *SimulatedBackendsTestSuite) SetupNotaryOnDestination() { + var err error + a.NotaryOnDestinationBondedWallet, err = wallet.FromRandom() + if err != nil { + a.T().Fatal(err) + } + a.NotaryOnDestinationBondedSigner = localsigner.NewSigner(a.NotaryOnDestinationBondedWallet.PrivateKey()) + + a.NotaryOnDestinationUnbondedWallet, err = wallet.FromRandom() + if err != nil { + a.T().Fatal(err) + } + a.NotaryOnDestinationUnbondedSigner = localsigner.NewSigner(a.NotaryOnDestinationUnbondedWallet.PrivateKey()) +} + // SetupExecutor sets up the Executor agent. func (a *SimulatedBackendsTestSuite) SetupExecutor() { var err error @@ -336,6 +360,7 @@ func (a *SimulatedBackendsTestSuite) SetupTest() { a.SetupGuard() a.SetupNotary() a.SetupNotaryOnOrigin() + a.SetupNotaryOnDestination() a.SetupExecutor() a.TestDeployManager = NewDeployManager(a.T()) @@ -389,8 +414,8 @@ func (a *SimulatedBackendsTestSuite) SetupTest() { a.GetTestContext(), a.TestBackendSummit, []backends.SimulatedTestBackend{a.TestBackendOrigin, a.TestBackendDestination}, - []common.Address{a.GuardBondedSigner.Address(), a.NotaryBondedSigner.Address(), a.NotaryOnOriginBondedSigner.Address()}, - []uint32{uint32(0), uint32(a.TestBackendDestination.GetChainID()), uint32(a.TestBackendOrigin.GetChainID())}) + []common.Address{a.GuardBondedSigner.Address(), a.NotaryBondedSigner.Address(), a.NotaryOnOriginBondedSigner.Address(), a.NotaryOnDestinationBondedSigner.Address()}, + []uint32{uint32(0), uint32(a.TestBackendDestination.GetChainID()), uint32(a.TestBackendOrigin.GetChainID()), uint32(a.TestBackendDestination.GetChainID())}) if err != nil { a.T().Fatal(err) } diff --git a/agents/types/agent_status_relayed_state.go b/agents/types/agent_status_relayed_state.go new file mode 100644 index 0000000000..6489af534f --- /dev/null +++ b/agents/types/agent_status_relayed_state.go @@ -0,0 +1,11 @@ +package types + +// AgentStatusRelayedState represents the state of a RelayableAgentStatus model. +type AgentStatusRelayedState uint8 + +const ( + // Queued is when an agent status has been updated on Summit, but has not been relayed to the remote chain. + Queued AgentStatusRelayedState = iota + // Relayed is when the agent status has been relayed to the remote chain. + Relayed +) diff --git a/agents/types/agent_tree.go b/agents/types/agent_tree.go index 1584b9dfed..bf7d4ef3ee 100644 --- a/agents/types/agent_tree.go +++ b/agents/types/agent_tree.go @@ -8,6 +8,10 @@ type AgentTree struct { AgentRoot string // AgentAddress is the address of the agent for the Merkle proof. AgentAddress common.Address + // AgentDomain is the domain of the agent. + AgentDomain uint32 + // UpdatedAgentFlag is the updated agent flag corresponding to the agent tree. + UpdatedAgentFlag AgentFlagType // BlockNumber is the block number that the agent tree was updated (on summit). BlockNumber uint64 // Proof is the agent tree proof.