diff --git a/agents/README.md b/agents/README.md index 79dfa20d12..385553146c 100644 --- a/agents/README.md +++ b/agents/README.md @@ -39,3 +39,26 @@ root └── types: Common agents types +# Testing Suite + +Tests for `agents` have setup hooks defined in `agents/testutil/simulated_backends_suite.go`. Any suite that embeds `SimulatedBackendsTestSuite` will have simulated backend and messaging contract scaffolding for Summit, Origin, and Desination chains. This includes `TestExecutorSuite`, `TestGuardSuite`, `TestNotarySuite`, `ExampleAgentSuite`, and `AgentsIntegrationSuite`. + +To run all agent tests: + +```bash +cd agents +go test -v ./... +``` + +To run an individual suite (for example, `TestExecutorSuite`): + +```bash +cd agents/executor +go test -v +``` + +To run an individual test (for example, `TestVerifyState`): +```bash +cd agents/executor +go test -v -run TestExecutorSuite/TestVerifyState +``` diff --git a/agents/agents/executor/executor_utils.go b/agents/agents/executor/executor_utils.go index 585c5a02a9..5133432c45 100644 --- a/agents/agents/executor/executor_utils.go +++ b/agents/agents/executor/executor_utils.go @@ -65,7 +65,7 @@ func (e Executor) logToSnapshot(log ethTypes.Log, chainID uint32) (types.Snapsho return nil, fmt.Errorf("could not parse snapshot: %w", err) } - if snapshotMetadata.Snapshot == nil || snapshotMetadata.AgentDomain == 0 { + if snapshotMetadata.Snapshot == nil || snapshotMetadata.AgentDomain() == 0 { //nolint:nilnil return nil, nil } diff --git a/agents/agents/guard/README.md b/agents/agents/guard/README.md new file mode 100644 index 0000000000..414edf8c25 --- /dev/null +++ b/agents/agents/guard/README.md @@ -0,0 +1,37 @@ +# Guard +The Guard is an agent responsible for verifying actions from other Agents in the optimistic messaging model. This includes polling for invalid states and attestations as well as submitting fraud reports. + +## Components +The Guard operates with four main components: +### Run +`streamLogs` is the data-getter for the Guard. It works by instantiating a gRPC connection to Scribe, and puts logs in a channel for the origin and destination contracts on each chain in the config. From here, it verifies the logs' order since the order of logs are very important for merkle tree construction. +
Additionally, if the Guard unexpectedly stops in the middle of streaming logs, it will use the current database state to reconstruct the tree up to where the last log was, then continue to use gRPC. +

`receiveLogs` is the data-processor for the Guard. It works by taking the logs streamed from `streamLogs` and parsing the logs into either a `Message` on the `Origin.sol` contract, or a `Attestation` on the `Destination.sol` contract. It then stores the data into the Guard's database and builds the tree accordingly. +

`loadOriginLatestStates` polls Origin states and caches them in order to make the latest data available. +

`submitLatestSnapshot` fetches the latest snapshot from Origin and submits it on Summit. +

`updateAgentStatuses` polls the database for `RelayableAgentStatus` entries and calls `updateAgentStatus()` once a sufficient agent root is passed to the given remote chain. + +### Fraud Reporting +The fraud reporting logic can be found in `fraud.go`, which consists mostly of handlers for various logs. The two major handlers are `handleSnapshotAccepted` and `handleAttestationAccepted`, both of which verify states corresponding to the incoming snapshot/attestation, initiate slashing if applicable, and submit state reports to eligible chains. + +## Usage + +Navigate to `sanguine/agents/agents/guard/main` and run the following command to start the Guard: + +```bash +$ go run main.go +``` +Then the Guard command line will be exposed. The Guard requires a gRPC connection to a Scribe instance to stream logs. This can be done with either a remote Scribe or an embedded Scribe. + +For more information on how to interact with Scribe see the Executor README. + +## Directory Structure + +
+Guard
+├── main: API server
+├── cmd: CLI commands
+├── db: Database interface
+│   └── sql: Database writer, reader, and migrations
+├── main: CLI entrypoint
+
diff --git a/agents/agents/guard/calls.go b/agents/agents/guard/calls.go new file mode 100644 index 0000000000..04ddd9dd06 --- /dev/null +++ b/agents/agents/guard/calls.go @@ -0,0 +1,218 @@ +package guard + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + ethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/synapsecns/sanguine/agents/types" + "github.com/synapsecns/sanguine/core/retry" + "github.com/synapsecns/sanguine/ethergo/signer/signer" +) + +type agentStatusContract interface { + // GetAgentStatus returns the current agent status for the given agent. + GetAgentStatus(ctx context.Context, address common.Address) (types.AgentStatus, error) +} + +// getAgentStatus fetches the agent status of an agent from the given chain. +func (g Guard) getAgentStatus(ctx context.Context, chainID uint32, agent common.Address) (agentStatus types.AgentStatus, err error) { + var contract agentStatusContract + if chainID == g.summitDomainID { + contract = g.domains[chainID].BondingManager() + } else { + contract = g.domains[chainID].LightManager() + } + contractCall := func(ctx context.Context) error { + agentStatus, err = contract.GetAgentStatus(ctx, agent) + if err != nil { + return fmt.Errorf("could not get agent status from contract: %w", err) + } + return nil + } + err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) + if err != nil { + return nil, fmt.Errorf("could not get agent status after retry: %w", err) + } + return agentStatus, nil +} + +// verifyState verifies a state on a given chain. +func (g Guard) verifyState(ctx context.Context, state types.State, stateIndex int, data types.StateValidationData) (err error) { + var submitFunc func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) + if types.HasAttestation(data) { + submitFunc = func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { + tx, err = g.domains[state.Origin()].LightInbox().VerifyStateWithAttestation( + transactor, + int64(stateIndex), + data.SnapshotPayload(), + data.AttestationPayload(), + data.AttestationSignature(), + ) + return + } + } else { + submitFunc = func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { + tx, err = g.domains[state.Origin()].LightInbox().VerifyStateWithSnapshot( + transactor, + int64(stateIndex), + data.SnapshotPayload(), + data.SnapshotSignature(), + ) + return + } + } + + // Ensure the agent that provided the snapshot is active on origin. + ok, err := g.ensureAgentActive(ctx, data.Agent(), state.Origin()) + if err != nil { + return fmt.Errorf("could not ensure agent is active: %w", err) + } + if !ok { + logger.Infof("Agent %s is not active on chain %d; not verifying snapshot state", data.Agent().Hex(), state.Origin()) + return nil + } + + _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(state.Origin())), submitFunc) + if err != nil { + return fmt.Errorf("could not verify state on chain %d: %w", state.Origin(), err) + } + return nil +} + +type stateReportContract interface { + // SubmitStateReportWithSnapshot reports to the inbox that a state within a snapshot is invalid. + SubmitStateReportWithSnapshot(transactor *bind.TransactOpts, stateIndex int64, signature signer.Signature, snapPayload []byte, snapSignature []byte) (tx *ethTypes.Transaction, err error) + // SubmitStateReportWithAttestation submits a state report corresponding to an attesation for an invalid state. + SubmitStateReportWithAttestation(transactor *bind.TransactOpts, stateIndex int64, signature signer.Signature, snapPayload, attPayload, attSignature []byte) (tx *ethTypes.Transaction, err error) +} + +// submitStateReport submits a state report to the given chain, provided a snapshot or attestation. +func (g Guard) submitStateReport(ctx context.Context, chainID uint32, state types.State, stateIndex int, data types.StateValidationData) (err error) { + var contract stateReportContract + if chainID == g.summitDomainID { + contract = g.domains[chainID].Inbox() + } else { + contract = g.domains[chainID].LightInbox() + } + + var submitFunc func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) + srSignature, _, _, err := state.SignState(ctx, g.bondedSigner) + if err != nil { + return fmt.Errorf("could not sign state: %w", err) + } + if types.HasAttestation(data) { + submitFunc = func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { + tx, err = contract.SubmitStateReportWithAttestation( + transactor, + int64(stateIndex), + srSignature, + data.SnapshotPayload(), + data.AttestationPayload(), + data.AttestationSignature(), + ) + return + } + } else { + submitFunc = func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { + tx, err = contract.SubmitStateReportWithSnapshot( + transactor, + int64(stateIndex), + srSignature, + data.SnapshotPayload(), + data.SnapshotSignature(), + ) + return + } + } + + // Ensure the agent that provided the snapshot is active on the agent's respective domain. + ok, err := g.ensureAgentActive(ctx, data.Agent(), chainID) + if err != nil { + return fmt.Errorf("could not ensure agent is active: %w", err) + } + if !ok { + logger.Infof("Agent %s is not active on chain %d; not verifying snapshot state", data.Agent().Hex(), chainID) + return nil + } + + _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(chainID)), submitFunc) + if err != nil { + return fmt.Errorf("could not submit state report to chain %d: %w", chainID, err) + } + return nil +} + +// getDisputeStatus fetches the dispute status of an agent from Summit. +func (g Guard) getDisputeStatus(ctx context.Context, agent common.Address) (status types.DisputeStatus, err error) { + contractCall := func(ctx context.Context) error { + status, err = g.domains[g.summitDomainID].BondingManager().GetDisputeStatus(ctx, agent) + if err != nil { + return fmt.Errorf("could not get dispute status: %w", err) + } + return nil + } + err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) + if err != nil { + return nil, fmt.Errorf("could not get dispute status: %w", err) + } + return status, nil +} + +// ensureAgentActive 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) ensureAgentActive(ctx context.Context, agent common.Address, chainID uint32) (ok bool, err error) { + agentStatus, err := g.getAgentStatus(ctx, chainID, 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") + } + // Fetch the agent status from Summit. + agentStatusSummit, err := g.getAgentStatus(ctx, g.summitDomainID, agent) + if err != nil { + return false, fmt.Errorf("could not get agent status: %w", err) + } + if agentStatusSummit.Flag() != types.AgentFlagActive && agentStatusSummit.Flag() != types.AgentFlagUnstaking { + return false, fmt.Errorf("agent is not active or unstaking on summit: %s [status=%s]", agent.Hex(), agentStatusSummit.Flag().String()) + } + // Update the agent status using the last known root on remote chain. + err = g.relayAgentStatus(ctx, agent, chainID, agentStatusSummit.Flag()) + if err != nil { + return false, err + } + return true, nil + case types.AgentFlagActive, types.AgentFlagUnstaking: + return true, nil + default: + return false, nil + } +} + +// relayAgentStatus relays an Active agent status from Summit to a remote +// chain where the agent is unknown. +func (g Guard) relayAgentStatus(ctx context.Context, agent common.Address, chainID uint32, flag types.AgentFlagType) error { + err := g.guardDB.StoreRelayableAgentStatus( + ctx, + agent, + types.AgentFlagUnknown, + flag, + chainID, + ) + if err != nil { + return fmt.Errorf("could not store relayable agent status: %w", err) + } + err = g.updateAgentStatus(ctx, chainID) + if err != nil { + return err + } + return nil +} diff --git a/agents/agents/guard/fraud.go b/agents/agents/guard/fraud.go index 7dee240103..62acf4e9ad 100644 --- a/agents/agents/guard/fraud.go +++ b/agents/agents/guard/fraud.go @@ -12,136 +12,40 @@ import ( "github.com/synapsecns/sanguine/core/retry" ) -// handleSnapshot checks a snapshot for invalid states. +// handleSnapshotAccepted checks a snapshot for invalid states. // If an invalid state is found, initiate slashing and submit a state report. // //nolint:cyclop,gocognit -func (g Guard) handleSnapshot(ctx context.Context, log ethTypes.Log) error { - fraudSnapshot, err := g.inboxParser.ParseSnapshotAccepted(log) +func (g Guard) handleSnapshotAccepted(ctx context.Context, log ethTypes.Log) error { + snapshotData, err := g.inboxParser.ParseSnapshotAccepted(log) if err != nil { return fmt.Errorf("could not parse snapshot accepted: %w", err) } - // Verify each state in the snapshot. - for si, s := range fraudSnapshot.Snapshot.States() { - stateIndex, state := si, s - isSlashable, err := g.isStateSlashable(ctx, state) - if err != nil { - return fmt.Errorf("could not handle state: %w", err) - } - if !isSlashable { - continue - } - - // Initiate slashing on origin. - _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(state.Origin())), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[state.Origin()].LightInbox().VerifyStateWithSnapshot( - transactor, - int64(stateIndex), - fraudSnapshot.Payload, - fraudSnapshot.Signature, - ) - if err != nil { - return nil, fmt.Errorf("could not verify state with snapshot: %w", err) - } - - return - }) - if err != nil { - return fmt.Errorf("could not submit VerifyStateWithSnapshot tx: %w", err) - } - - // Check if we should submit the state report. - shouldSubmit, err := g.shouldSubmitStateReport(ctx, fraudSnapshot) - if err != nil { - return fmt.Errorf("could not check if should submit state report: %w", err) - } - if !shouldSubmit { - return nil - } - - // 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.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(g.summitDomainID)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[g.summitDomainID].Inbox().SubmitStateReportWithSnapshot( - transactor, - int64(stateIndex), - srSignature, - fraudSnapshot.Payload, - fraudSnapshot.Signature, - ) - if err != nil { - return nil, fmt.Errorf("could not submit state report with snapshot to summit: %w", err) - } - - return - }) - if err != nil { - return fmt.Errorf("could not submit SubmitStateReportWithSnapshot tx: %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.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(fraudSnapshot.AgentDomain)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[fraudSnapshot.AgentDomain].LightInbox().SubmitStateReportWithSnapshot( - transactor, - int64(stateIndex), - srSignature, - fraudSnapshot.Payload, - fraudSnapshot.Signature, - ) - if err != nil { - return nil, fmt.Errorf("could not submit state report with snapshot to agent domain %d: %w", fraudSnapshot.AgentDomain, err) - } - - return - }) - if err != nil { - return fmt.Errorf("could not submit SubmitStateReportWithSnapshot tx: %w", err) - } + err = g.handleSnapshot(ctx, snapshotData.Snapshot, snapshotData) + if err != nil { + return fmt.Errorf("could not handle snapshot: %w", err) } - return nil } -// Only submit a state report if we are not on Summit, and the snapshot -// agent is not currently in dispute. -func (g Guard) shouldSubmitStateReport(ctx context.Context, snapshot *types.FraudSnapshot) (bool, error) { - var disputeStatus types.DisputeStatus - var err error - contractCall := func(ctx context.Context) error { - disputeStatus, err = g.domains[g.summitDomainID].BondingManager().GetDisputeStatus(ctx, snapshot.Agent) +// Determine which chains need to receive the state report. +// A chain should receive a state report if the fraudulent Notary has not yet been reported on that chain- +// That is, a dispute has not yet been opened with this notary on that chain. +func (g Guard) getStateReportChains(ctx context.Context, chainID uint32, agent common.Address) ([]uint32, error) { + stateReportChains := []uint32{} + for _, reportChainID := range []uint32{g.summitDomainID, chainID} { + status, err := g.getDisputeStatus(ctx, agent) if err != nil { - return fmt.Errorf("could not get dispute status: %w", err) + return []uint32{}, err + } + isNotary := reportChainID != 0 + isNotInDispute := status.Flag() == types.DisputeFlagNone + if isNotary && isNotInDispute { + stateReportChains = append(stateReportChains, reportChainID) } - - return nil - } - err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) - if err != nil { - return false, fmt.Errorf("could not get dispute status: %w", err) } - - isNotary := snapshot.AgentDomain != 0 - isNotInDispute := disputeStatus.Flag() == types.DisputeFlagNone - shouldSubmit := isNotary && isNotInDispute - return shouldSubmit, nil + return stateReportChains, nil } // isStateSlashable checks if a state is slashable, i.e. if the state is valid on the @@ -169,17 +73,17 @@ func (g Guard) isStateSlashable(ctx context.Context, state types.State) (bool, e return !isValid, nil } -// handleAttestation checks whether an attestation is valid. +// handleAttestationAccepted checks whether an attestation is valid. // If invalid, initiate slashing and/or submit a fraud report. -func (g Guard) handleAttestation(ctx context.Context, log ethTypes.Log) error { - fraudAttestation, err := g.lightInboxParser.ParseAttestationAccepted(log) +func (g Guard) handleAttestationAccepted(ctx context.Context, log ethTypes.Log) error { + attestationData, err := g.lightInboxParser.ParseAttestationAccepted(log) if err != nil { return fmt.Errorf("could not parse attestation accepted: %w", err) } var isValid bool contractCall := func(ctx context.Context) error { - isValid, err = g.domains[g.summitDomainID].Summit().IsValidAttestation(ctx, fraudAttestation.Payload) + isValid, err = g.domains[g.summitDomainID].Summit().IsValidAttestation(ctx, attestationData.AttestationPayload()) if err != nil { return fmt.Errorf("could not check validity of attestation: %w", err) } @@ -192,22 +96,22 @@ func (g Guard) handleAttestation(ctx context.Context, log ethTypes.Log) error { } if isValid { - return g.handleValidAttestation(ctx, fraudAttestation) + return g.handleValidAttestation(ctx, attestationData) } - return g.handleInvalidAttestation(ctx, fraudAttestation) + return g.handleInvalidAttestation(ctx, attestationData) } // handleValidAttestation handles an attestation that is valid, but may // attest to a snapshot that contains an invalid state. // //nolint:cyclop,gocognit -func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *types.FraudAttestation) error { +func (g Guard) handleValidAttestation(ctx context.Context, attestationData *types.AttestationWithMetadata) error { // Fetch the attested snapshot. var snapshot types.Snapshot var err error contractCall := func(ctx context.Context) error { - snapshot, err = g.domains[g.summitDomainID].Summit().GetNotarySnapshot(ctx, fraudAttestation.Payload) + snapshot, err = g.domains[g.summitDomainID].Summit().GetNotarySnapshot(ctx, attestationData.AttestationPayload()) if err != nil { return fmt.Errorf("could not get snapshot: %w", err) } @@ -219,180 +123,69 @@ func (g Guard) handleValidAttestation(ctx context.Context, fraudAttestation *typ return fmt.Errorf("could not get snapshot: %w", err) } - snapPayload, err := snapshot.Encode() + // Set the SnapshotPayload so that it can be passed to contract calls. + snapshotPayload, err := snapshot.Encode() if err != nil { return fmt.Errorf("could not encode snapshot: %w", err) } + attestationData.SetSnapshotPayload(snapshotPayload) + + err = g.handleSnapshot(ctx, snapshot, attestationData) + if err != nil { + return fmt.Errorf("could not handle snapshot: %w", err) + } + + return nil +} - // Verify each state in the snapshot. +// handleSnapshot handles a snapshot by validating each state in the snapshot. +// If an invalid state is found, initiate slashing and submit state reports on eligible chains. +func (g Guard) handleSnapshot(ctx context.Context, snapshot types.Snapshot, data types.StateValidationData) error { + // Process each state in the snapshot. for si, s := range snapshot.States() { stateIndex, state := si, s isSlashable, err := g.isStateSlashable(ctx, state) if err != nil { - return fmt.Errorf("could not check if state is slashable: %w", err) + return fmt.Errorf("could not handle state: %w", err) } if !isSlashable { continue } // Initiate slashing on origin. - _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(state.Origin())), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[state.Origin()].LightInbox().VerifyStateWithAttestation( - transactor, - int64(stateIndex), - snapPayload, - fraudAttestation.Payload, - fraudAttestation.Signature, - ) - if err != nil { - return nil, fmt.Errorf("could not verify state with attestation: %w", err) - } - - return - }) - - if err != nil { - return fmt.Errorf("could not submit VerifyStateWithAttestation tx: %w", err) - } - - // 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) + err = g.verifyState(ctx, state, stateIndex, data) if err != nil { - return fmt.Errorf("could not prepare state report on summit: %w", err) - } - if !ok { - continue + return fmt.Errorf("could not verify state: %w", err) } - _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(g.summitDomainID)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[g.summitDomainID].Inbox().SubmitStateReportWithAttestation( - transactor, - int64(stateIndex), - srSignature, - snapPayload, - fraudAttestation.Payload, - fraudAttestation.Signature, - ) - if err != nil { - return nil, fmt.Errorf("could not submit state report with attestation on summit: %w", err) - } - return - }) + // Evaluate which chains need a state report. + stateReportChains, err := g.getStateReportChains(ctx, data.AgentDomain(), data.Agent()) if err != nil { - return fmt.Errorf("could not submit SubmitStateReportWithAttestation tx: %w", err) + return fmt.Errorf("could not get state report chains: %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.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(fraudAttestation.AgentDomain)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[fraudAttestation.AgentDomain].LightInbox().SubmitStateReportWithAttestation( - transactor, - int64(stateIndex), - srSignature, - snapPayload, - fraudAttestation.Payload, - fraudAttestation.Signature, - ) + // Submit the state report on each eligible chain. + // If a notary has not been reported anywhere, + // report should be submitted on both summit and remote. + for _, chainID := range stateReportChains { + err = g.submitStateReport(ctx, chainID, state, stateIndex, data) if err != nil { - return nil, fmt.Errorf("could not submit state report with attestation on agent domain %d: %w", fraudAttestation.AgentDomain, err) + return fmt.Errorf("could not submit state report: %w", err) } - - return - }) - if err != nil { - return fmt.Errorf("could not submit SubmitStateReportWithAttestation tx: %w", 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. -// -//nolint:cyclop -func (g Guard) prepareStateReport(ctx context.Context, agent common.Address, chainID uint32) (ok bool, err error) { - var agentStatus types.AgentStatus - //nolint:nestif - if chainID == g.summitDomainID { - contractCall := func(ctx context.Context) error { - agentStatus, err = g.domains[chainID].BondingManager().GetAgentStatus(ctx, agent) - if err != nil { - return fmt.Errorf("could not get agent status: %w", err) - } - - return nil - } - err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) - if err != nil { - return false, fmt.Errorf("could not get agent status: %w", err) - } - } else { - contractCall := func(ctx context.Context) error { - agentStatus, err = g.domains[chainID].LightManager().GetAgentStatus(ctx, agent) - if err != nil { - return fmt.Errorf("could not get agent status: %w", err) - } - - return nil - } - err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) - if err != nil { - return false, fmt.Errorf("could not get agent status: %w", err) - } - } - 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 { +func (g Guard) handleInvalidAttestation(ctx context.Context, attestationData *types.AttestationWithMetadata) error { // Initiate slashing for invalid attestation. _, err := g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(g.summitDomainID)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { tx, err = g.domains[g.summitDomainID].Inbox().VerifyAttestation( transactor, - fraudAttestation.Payload, - fraudAttestation.Signature, + attestationData.AttestationPayload(), + attestationData.AttestationSignature(), ) if err != nil { return nil, fmt.Errorf("could not verify attestation: %w", err) @@ -405,7 +198,7 @@ func (g Guard) handleInvalidAttestation(ctx context.Context, fraudAttestation *t } // Submit a fraud report by calling `submitAttestationReport()` on the remote chain. - arSignature, _, _, err := fraudAttestation.Attestation.SignAttestation(ctx, g.bondedSigner, false) + arSignature, _, _, err := attestationData.Attestation.SignAttestation(ctx, g.bondedSigner, false) if err != nil { return fmt.Errorf("could not sign attestation: %w", err) } @@ -413,12 +206,12 @@ func (g Guard) handleInvalidAttestation(ctx context.Context, fraudAttestation *t if err != nil { return fmt.Errorf("could not encode signature: %w", err) } - _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(fraudAttestation.AgentDomain)), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { - tx, err = g.domains[fraudAttestation.AgentDomain].LightInbox().SubmitAttestationReport( + _, err = g.txSubmitter.SubmitTransaction(ctx, big.NewInt(int64(attestationData.AgentDomain())), func(transactor *bind.TransactOpts) (tx *ethTypes.Transaction, err error) { + tx, err = g.domains[attestationData.AgentDomain()].LightInbox().SubmitAttestationReport( transactor, - fraudAttestation.Payload, + attestationData.AttestationPayload(), arSignatureEncoded, - fraudAttestation.Signature, + attestationData.AttestationSignature(), ) if err != nil { return nil, fmt.Errorf("could not submit attestation report: %w", err) @@ -433,8 +226,10 @@ func (g Guard) handleInvalidAttestation(ctx context.Context, fraudAttestation *t return nil } +// handleReceiptAccepted checks whether a receipt is valid and submits a receipt report if not. +// //nolint:cyclop -func (g Guard) handleReceipt(ctx context.Context, log ethTypes.Log) error { +func (g Guard) handleReceiptAccepted(ctx context.Context, log ethTypes.Log) error { fraudReceipt, err := g.inboxParser.ParseReceiptAccepted(log) if err != nil { return fmt.Errorf("could not parse receipt accepted: %w", err) @@ -598,7 +393,6 @@ 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, @@ -622,16 +416,7 @@ func (g Guard) handleStatusUpdated(ctx context.Context, log ethTypes.Log, chainI } if statusUpdated.Domain != 0 { - // Fetch the current remote status and check whether the status is synced. - contractCall := func(ctx context.Context) error { - remoteStatus, err = g.domains[statusUpdated.Domain].LightManager().GetAgentStatus(ctx, statusUpdated.Agent) - if err != nil { - return fmt.Errorf("could not get agent status: %w", err) - } - - return nil - } - err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) + remoteStatus, err := g.getAgentStatus(ctx, statusUpdated.Domain, statusUpdated.Agent) if err != nil { return fmt.Errorf("could not get agent status: %w", err) } @@ -740,16 +525,7 @@ func (g Guard) updateAgentStatus(ctx context.Context, chainID uint32) error { if localRootBlockNumber >= treeBlockNumber { logger.Infof("Relaying agent status for agent %s on chain %d", tree.AgentAddress.String(), chainID) // Fetch the agent status to be relayed from Summit. - var agentStatus types.AgentStatus - contractCall := func(ctx context.Context) error { - agentStatus, err = g.domains[g.summitDomainID].BondingManager().GetAgentStatus(ctx, tree.AgentAddress) - if err != nil { - return fmt.Errorf("could not get agent status: %w", err) - } - - return nil - } - err = retry.WithBackoff(ctx, contractCall, g.retryConfig...) + agentStatus, err := g.getAgentStatus(ctx, g.summitDomainID, tree.AgentAddress) if err != nil { return fmt.Errorf("could not get agent status: %w", err) } diff --git a/agents/agents/guard/fraud_test.go b/agents/agents/guard/fraud_test.go index 8bd012fb44..60ba061b02 100644 --- a/agents/agents/guard/fraud_test.go +++ b/agents/agents/guard/fraud_test.go @@ -330,14 +330,14 @@ func (g GuardSuite) TestFraudulentAttestationOnDestination() { Nil(g.T(), err) dataHash, err := attestationContract.DataHash(&bind.CallOpts{Context: g.GetTestContext()}, agentRoot, snapGasHash) Nil(g.T(), err) - fraudAttestation := types.NewAttestation( + attestationData := types.NewAttestation( common.BigToHash(big.NewInt(int64(gofakeit.Int32()))), dataHash, 1, big.NewInt(int64(gofakeit.Int32())), big.NewInt(int64(gofakeit.Int32())), ) - attSignature, attEncoded, _, err := fraudAttestation.SignAttestation(g.GetTestContext(), g.NotaryBondedSigner, true) + attSignature, attEncoded, _, err := attestationData.SignAttestation(g.GetTestContext(), g.NotaryBondedSigner, true) Nil(g.T(), err) // Before submitting the attestation, ensure that there are no disputes opened. diff --git a/agents/agents/guard/guard.go b/agents/agents/guard/guard.go index 1c7f3e7e70..929978b4df 100644 --- a/agents/agents/guard/guard.go +++ b/agents/agents/guard/guard.go @@ -255,11 +255,11 @@ func (g Guard) receiveLogs(ctx context.Context, chainID uint32) error { func (g Guard) handleLog(ctx context.Context, log ethTypes.Log, chainID uint32) error { switch { case isSnapshotAcceptedEvent(g.inboxParser, log): - return g.handleSnapshot(ctx, log) + return g.handleSnapshotAccepted(ctx, log) case isAttestationAcceptedEvent(g.lightInboxParser, log): - return g.handleAttestation(ctx, log) + return g.handleAttestationAccepted(ctx, log) case isReceiptAcceptedEvent(g.inboxParser, log): - return g.handleReceipt(ctx, log) + return g.handleReceiptAccepted(ctx, log) case isStatusUpdatedEvent(g.bondingManagerParser, log): return g.handleStatusUpdated(ctx, log, chainID) case isRootUpdatedEvent(g.bondingManagerParser, log): diff --git a/agents/contracts/inbox/parser.go b/agents/contracts/inbox/parser.go index bb6d78b14e..a9c9ad9f7a 100644 --- a/agents/contracts/inbox/parser.go +++ b/agents/contracts/inbox/parser.go @@ -13,7 +13,7 @@ type Parser interface { // EventType is the event type. EventType(log ethTypes.Log) (_ EventType, ok bool) // ParseSnapshotAccepted parses a SnapshotAccepted event. - ParseSnapshotAccepted(log ethTypes.Log) (_ *types.FraudSnapshot, err error) + ParseSnapshotAccepted(log ethTypes.Log) (_ *types.SnapshotWithMetadata, err error) // ParseReceiptAccepted parses a ReceiptAccepted event. ParseReceiptAccepted(log ethTypes.Log) (_ *InboxReceiptAccepted, err error) } @@ -47,18 +47,18 @@ func (p parserImpl) EventType(log ethTypes.Log) (_ EventType, ok bool) { } // ParseSnapshotAccepted parses a SnapshotAccepted event. -func (p parserImpl) ParseSnapshotAccepted(log ethTypes.Log) (_ *types.FraudSnapshot, err error) { +func (p parserImpl) ParseSnapshotAccepted(log ethTypes.Log) (_ *types.SnapshotWithMetadata, err error) { inboxSnapshot, err := p.filterer.ParseSnapshotAccepted(log) if err != nil { return nil, fmt.Errorf("could not parse snapshot accepted event: %w", err) } - fraudSnapshot, err := types.NewFraudSnapshotFromPayload(inboxSnapshot.SnapPayload, inboxSnapshot.Domain, inboxSnapshot.Agent, inboxSnapshot.SnapSignature) + snapshotData, err := types.NewSnapshotWithMetadata(inboxSnapshot.SnapPayload, inboxSnapshot.Domain, inboxSnapshot.Agent, inboxSnapshot.SnapSignature) if err != nil { return nil, fmt.Errorf("could not create fraud snapshot from payload: %w", err) } - return fraudSnapshot, nil + return snapshotData, nil } // ParseReceiptAccepted parses a ReceiptAccepted event. diff --git a/agents/contracts/lightinbox/parser.go b/agents/contracts/lightinbox/parser.go index 5e29072630..d93940a60d 100644 --- a/agents/contracts/lightinbox/parser.go +++ b/agents/contracts/lightinbox/parser.go @@ -13,7 +13,7 @@ type Parser interface { // EventType determines if an event was initiated by the bridge or the user. EventType(log ethTypes.Log) (_ EventType, ok bool) // ParseAttestationAccepted parses an AttestationAccepted event - ParseAttestationAccepted(log ethTypes.Log) (_ *types.FraudAttestation, err error) + ParseAttestationAccepted(log ethTypes.Log) (_ *types.AttestationWithMetadata, err error) } type parserImpl struct { @@ -45,23 +45,23 @@ func (p parserImpl) EventType(log ethTypes.Log) (_ EventType, ok bool) { } // ParseAttestationAccepted parses an AttestationAccepted event. -func (p parserImpl) ParseAttestationAccepted(log ethTypes.Log) (_ *types.FraudAttestation, err error) { +func (p parserImpl) ParseAttestationAccepted(log ethTypes.Log) (_ *types.AttestationWithMetadata, err error) { lightInboxAttestationAccepted, err := p.filterer.ParseAttestationAccepted(log) if err != nil { return nil, fmt.Errorf("could not parse attestation accepted: %w", err) } - fraudAttestation, err := types.NewFraudAttestationFromPayload( + attestationData, err := types.NewAttestationWithMetadata( lightInboxAttestationAccepted.AttPayload, lightInboxAttestationAccepted.Domain, lightInboxAttestationAccepted.Notary, lightInboxAttestationAccepted.AttSignature, ) if err != nil { - return nil, fmt.Errorf("could not create fraud attestation from payload: %w", err) + return nil, fmt.Errorf("could not create attestation with metadata from payload: %w", err) } - return fraudAttestation, nil + return attestationData, nil } // EventType is the type of the light inbox event diff --git a/agents/types/fraud_attestation.go b/agents/types/fraud_attestation.go deleted file mode 100644 index 2b1a14be97..0000000000 --- a/agents/types/fraud_attestation.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -import "github.com/ethereum/go-ethereum/common" - -// FraudAttestation is an attestation that was submitted by a Notary and was deemed fraudulent. -type FraudAttestation struct { - // Attestation is the underlying attestation. - Attestation Attestation - // AgentDomain is the domain of the Notary who signed the attestation. - AgentDomain uint32 - // Notary is the Notary who signed and submitted the attestation. - Notary common.Address - // Payload is the attestation payload. - Payload []byte - // Signature is the signature of the attestation payload signed by the Notary. - Signature []byte -} - -// NewFraudAttestationFromPayload creates a new FraudAttestation from the attestation payload, domain, notary and attestation signature. -func NewFraudAttestationFromPayload(attestationPayload []byte, agentDomain uint32, notary common.Address, attSignature []byte) (*FraudAttestation, error) { - decodedAttestation, err := DecodeAttestation(attestationPayload) - if err != nil { - return nil, err - } - - return &FraudAttestation{ - Attestation: decodedAttestation, - AgentDomain: agentDomain, - Notary: notary, - Payload: attestationPayload, - Signature: attSignature, - }, nil -} diff --git a/agents/types/fraud_snapshot.go b/agents/types/fraud_snapshot.go deleted file mode 100644 index ea4abe1b56..0000000000 --- a/agents/types/fraud_snapshot.go +++ /dev/null @@ -1,37 +0,0 @@ -package types - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" -) - -// FraudSnapshot is a snapshot type with additional metadata for fraud handling. -type FraudSnapshot struct { - // Snapshot is the underlying snapshot. - Snapshot Snapshot - // AgentDomain is the domain of the agent that submitted the snapshot. - AgentDomain uint32 - // Agent is the agent that signed the snapshot. - Agent common.Address - // Payload is the snapshot payload. - Payload []byte - // Signature is the signature of the snapshot payload signed by the signer. - Signature []byte -} - -// NewFraudSnapshotFromPayload returns a new FraudSnapshot from a Snapshot payload and other metadata. -func NewFraudSnapshotFromPayload(snapshotPayload []byte, agentDomain uint32, agent common.Address, snapSignature []byte) (*FraudSnapshot, error) { - decodedSnapshot, err := DecodeSnapshot(snapshotPayload) - if err != nil { - return nil, fmt.Errorf("could not decode snapshot: %w", err) - } - - return &FraudSnapshot{ - Snapshot: decodedSnapshot, - AgentDomain: agentDomain, - Agent: agent, - Payload: snapshotPayload, - Signature: snapSignature, - }, nil -} diff --git a/agents/types/state_validation_data.go b/agents/types/state_validation_data.go new file mode 100644 index 0000000000..4312cd9dec --- /dev/null +++ b/agents/types/state_validation_data.go @@ -0,0 +1,147 @@ +package types + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +// StateValidationData provides data necessary for processing states in a snapshot, +// i.e. verifyState() and submitStateReport() contract calls. +type StateValidationData interface { + // Agent returns the agent that submitted the snapshot or attestation. + Agent() common.Address + // AgentDomain returns the domain of the agent that submitted the snapshot or attestation. + AgentDomain() uint32 + // SnapshotPayload returns the snapshot payload. + SnapshotPayload() []byte + // SnapshotSignature returns the snapshot signature, if it exists. + SnapshotSignature() []byte + // AttestationPayload returns the attestation payload, if it exists. + AttestationPayload() []byte + // AttestationSignature returns the attestation signature, if it exists. + AttestationSignature() []byte +} + +// HasAttestation indicates whether the data corresponds to an attestation (or a snapshot, if false). +func HasAttestation(data StateValidationData) bool { + return data.AttestationPayload() != nil +} + +// SnapshotWithMetadata is a snapshot type with additional metadata for fraud handling. +type SnapshotWithMetadata struct { + // Snapshot is the underlying snapshot. + Snapshot Snapshot + agentDomain uint32 + agent common.Address + snapshotPayload []byte + snapshotSignature []byte +} + +// NewSnapshotWithMetadata returns a new SnapshotWithMetadata from a Snapshot payload and other metadata. +func NewSnapshotWithMetadata(snapshotPayload []byte, agentDomain uint32, agent common.Address, snapshotSignature []byte) (*SnapshotWithMetadata, error) { + decodedSnapshot, err := DecodeSnapshot(snapshotPayload) + if err != nil { + return nil, fmt.Errorf("could not decode snapshot: %w", err) + } + + return &SnapshotWithMetadata{ + Snapshot: decodedSnapshot, + agentDomain: agentDomain, + agent: agent, + snapshotPayload: snapshotPayload, + snapshotSignature: snapshotSignature, + }, nil +} + +// Agent returns the agent that submitted the snapshot. +func (s *SnapshotWithMetadata) Agent() common.Address { + return s.agent +} + +// AgentDomain returns the domain of the agent that submitted the snapshot. +func (s *SnapshotWithMetadata) AgentDomain() uint32 { + return s.agentDomain +} + +// SnapshotPayload returns the snapshot payload. +func (s *SnapshotWithMetadata) SnapshotPayload() []byte { + return s.snapshotPayload +} + +// SnapshotSignature returns the snapshot signature. +func (s *SnapshotWithMetadata) SnapshotSignature() []byte { + return s.snapshotSignature +} + +// AttestationPayload returns nil, since the data corresponds to a snapshot. +func (s *SnapshotWithMetadata) AttestationPayload() []byte { + return nil +} + +// AttestationSignature returns nil, since the data corresponds to a snapshot. +func (s *SnapshotWithMetadata) AttestationSignature() []byte { + return nil +} + +// AttestationWithMetadata is an attestation that was submitted by a Notary and was deemed fraudulent. +type AttestationWithMetadata struct { + // Attestation is the underlying attestation. + Attestation Attestation + agentDomain uint32 + notary common.Address + attestationPayload []byte + attestationSignature []byte + snapshotPayload []byte +} + +// NewAttestationWithMetadata creates a new AttestationWithMetadata from the attestation payload, domain, notary and attestation signature. +func NewAttestationWithMetadata(attestationPayload []byte, agentDomain uint32, notary common.Address, attSignature []byte) (*AttestationWithMetadata, error) { + decodedAttestation, err := DecodeAttestation(attestationPayload) + if err != nil { + return nil, err + } + + return &AttestationWithMetadata{ + Attestation: decodedAttestation, + agentDomain: agentDomain, + notary: notary, + attestationPayload: attestationPayload, + attestationSignature: attSignature, + }, nil +} + +// SetSnapshotPayload sets the snapshot payload. +func (a *AttestationWithMetadata) SetSnapshotPayload(snapshotPayload []byte) { + a.snapshotPayload = snapshotPayload +} + +// Agent returns the agent that submitted the attestation. +func (a *AttestationWithMetadata) Agent() common.Address { + return a.notary +} + +// AgentDomain returns the domain of the agent that submitted the attestation. +func (a *AttestationWithMetadata) AgentDomain() uint32 { + return a.agentDomain +} + +// SnapshotPayload returns the snapshot payload. +func (a *AttestationWithMetadata) SnapshotPayload() []byte { + return a.snapshotPayload +} + +// SnapshotSignature returns the nil, since the data corresponds to an attestation. +func (a *AttestationWithMetadata) SnapshotSignature() []byte { + return nil +} + +// AttestationPayload returns the attestation payload. +func (a *AttestationWithMetadata) AttestationPayload() []byte { + return a.attestationPayload +} + +// AttestationSignature returns the attestation signature. +func (a *AttestationWithMetadata) AttestationSignature() []byte { + return a.attestationSignature +}