Skip to content

Commit

Permalink
Fraud reporting improvements (#1375)
Browse files Browse the repository at this point in the history
* Feat: add getStateReportChains()

* Feat: prepareStateReport -> ensureAgentActive, use it before verifying states

* Feat: ensureAgentActive called before verfication, but no longer need for state reports

* Feat: add calls.go with chain-agnostic funcs

* Feat: use submitStateReport() helper

* Feat: add verifyState() helper

* Cleanup: rename handleX() -> handleXAccepted()

* Feat: add handleSnapshot() helper

* WIP: coalesce FraudSnapshot and FraudAttestation into StateValidationData iface

* Feat: encorporate StateValidationData usage in contract calls

* Cleanup: FraudSnapshot -> SnapshotWithMetadata, FraudAttestation -> AttestationWithMetadata

* Cleanup: consistent data field naming, add comments

* Cleanup: remove logs

* Cleanup: lint

* Feat: use getAgentStatus() helper

* Cleanup: unused param

* Cleanup: move getDisputeStatus() to calls.go, add comments

* Cleanup: move ensureAgentActive() helper to calls.go

* Cleanup: move relayActiveAgentStatus() helper to calls.go

* Cleanup: remove log

* Fix: use ptr in data assign

* Cleanup: define SnapshotWithMetadata first

* Cleanup: better err msg

* Feat: fetch agent status from summit instead of assuming Active in relayAgentStatus()

* Cleanup: chainID shadowing

* Cleanup: err msg verbosity

* Cleanup: add guard README

* Cleanup: add testing docs to agents README

* Cleanup: remove 'Testing Suite' section from guard README
  • Loading branch information
dwasse authored Oct 10, 2023
1 parent 807eb2b commit cd4aaef
Show file tree
Hide file tree
Showing 12 changed files with 508 additions and 377 deletions.
23 changes: 23 additions & 0 deletions agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,26 @@ root
└── <a href="./types">types</a>: Common agents types
</pre>

# 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
```
2 changes: 1 addition & 1 deletion agents/agents/executor/executor_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 37 additions & 0 deletions agents/agents/guard/README.md
Original file line number Diff line number Diff line change
@@ -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.
<br /> 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.
<br /> <br > `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.
<br /> <br > `loadOriginLatestStates` polls Origin states and caches them in order to make the latest data available.
<br /> <br > `submitLatestSnapshot` fetches the latest snapshot from Origin and submits it on Summit.
<br /> <br > `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

<pre>
Guard
├── <a href="./api">main</a>: API server
├── <a href="./cmd">cmd</a>: CLI commands
├── <a href="./db">db</a>: Database interface
│ └── <a href="db/sql">sql</a>: Database writer, reader, and migrations
├── <a href="./main">main</a>: CLI entrypoint
</pre>
218 changes: 218 additions & 0 deletions agents/agents/guard/calls.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit cd4aaef

Please sign in to comment.