-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fraud reporting improvements (#1375)
* 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
Showing
12 changed files
with
508 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.