wormhole/node/pkg/processor/observation.go

266 lines
10 KiB
Go

package processor
import (
"context"
"encoding/hex"
"fmt"
bridge_common "github.com/certusone/wormhole/node/pkg/common"
"github.com/certusone/wormhole/node/pkg/reporter"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"go.uber.org/zap"
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
"github.com/certusone/wormhole/node/pkg/vaa"
)
var (
observationsReceivedTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "wormhole_observations_received_total",
Help: "Total number of raw VAA observations received from gossip",
})
observationsReceivedByGuardianAddressTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_observations_signed_by_guardian_total",
Help: "Total number of signed and verified VAA observations grouped by guardian address",
}, []string{"addr"})
observationsFailedTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_observations_verification_failures_total",
Help: "Total number of observations verification failure, grouped by failure reason",
}, []string{"cause"})
observationsUnknownTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "wormhole_observations_unknown_total",
Help: "Total number of verified observations we haven't seen ourselves",
})
observationsDirectSubmissionsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_observations_direct_submissions_queued_total",
Help: "Total number of observations for a specific target chain that were queued for direct submission",
}, []string{"target_chain"})
observationsDirectSubmissionSuccessTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_observations_direct_submission_success_total",
Help: "Total number of observations for a specific target chain that succeeded",
}, []string{"target_chain"})
)
// handleObservation processes a remote VAA observation, verifies it, checks whether the VAA has met quorum,
// and assembles and submits a valid VAA if possible.
func (p *Processor) handleObservation(ctx context.Context, m *gossipv1.SignedObservation) {
// SECURITY: at this point, observations received from the p2p network are fully untrusted (all fields!)
//
// Note that observations are never tied to the (verified) p2p identity key - the p2p network
// identity is completely decoupled from the guardian identity, p2p is just transport.
hash := hex.EncodeToString(m.Hash)
p.logger.Info("received observation",
zap.String("digest", hash),
zap.String("signature", hex.EncodeToString(m.Signature)),
zap.String("addr", hex.EncodeToString(m.Addr)))
observationsReceivedTotal.Inc()
// Verify the Guardian's signature. This verifies that m.Signature matches m.Hash and recovers
// the public key that was used to sign the payload.
pk, err := crypto.Ecrecover(m.Hash, m.Signature)
if err != nil {
p.logger.Warn("failed to verify signature on observation",
zap.String("digest", hash),
zap.String("signature", hex.EncodeToString(m.Signature)),
zap.String("addr", hex.EncodeToString(m.Addr)),
zap.Error(err))
observationsFailedTotal.WithLabelValues("invalid_signature").Inc()
return
}
// Verify that m.Addr matches the public key that signed m.Hash.
their_addr := common.BytesToAddress(m.Addr)
signer_pk := common.BytesToAddress(crypto.Keccak256(pk[1:])[12:])
if their_addr != signer_pk {
p.logger.Info("invalid observation - address does not match pubkey",
zap.String("digest", hash),
zap.String("signature", hex.EncodeToString(m.Signature)),
zap.String("addr", hex.EncodeToString(m.Addr)),
zap.String("pk", signer_pk.Hex()))
observationsFailedTotal.WithLabelValues("pubkey_mismatch").Inc()
return
}
// Determine which guardian set to use. The following cases are possible:
//
// - We have already seen the message and generated ourVAA. In this case, use the guardian set valid at the time,
// even if the guardian set was updated. Old guardian sets remain valid for longer than aggregation state,
// and the guardians in the old set stay online and observe and sign messages for the transition period.
//
// - We have not yet seen the message. In this case, we assume the latest guardian set because that's what
// we will store once we do see the message.
//
// This ensures that during a guardian set update, a node which observed a given message with either the old
// or the new guardian set can achieve consensus, since both the old and the new set would achieve consensus,
// assuming that 2/3+ of the old and the new guardian set have seen the message and will periodically attempt
// to retransmit their observations such that nodes who initially dropped the signature will get a 2nd chance.
//
// During an update, vaaState.signatures can contain signatures from *both* guardian sets.
//
var gs *bridge_common.GuardianSet
if p.state.vaaSignatures[hash] != nil && p.state.vaaSignatures[hash].gs != nil {
gs = p.state.vaaSignatures[hash].gs
} else {
gs = p.gs
}
// We haven't yet observed the trusted guardian set on Ethereum, and therefore, it's impossible to verify it.
// May as well not have received it/been offline - drop it and wait for the guardian set.
if gs == nil {
p.logger.Warn("dropping observations since we haven't initialized our guardian set yet",
zap.String("digest", hash),
zap.String("their_addr", their_addr.Hex()),
)
observationsFailedTotal.WithLabelValues("uninitialized_guardian_set").Inc()
return
}
// Verify that m.Addr is included in the guardian set. If it's not, drop the message. In case it's us
// who have the outdated guardian set, we'll just wait for the message to be retransmitted eventually.
_, ok := gs.KeyIndex(their_addr)
if !ok {
p.logger.Warn("received observation by unknown guardian - is our guardian set outdated?",
zap.String("digest", hash),
zap.String("their_addr", their_addr.Hex()),
zap.Uint32("index", gs.Index),
zap.Any("keys", gs.KeysAsHexStrings()),
)
observationsFailedTotal.WithLabelValues("unknown_guardian").Inc()
return
}
// Hooray! Now, we have verified all fields on SignedObservation and know that it includes
// a valid signature by an active guardian. We still don't fully trust them, as they may be
// byzantine, but now we know who we're dealing with.
// We can now count events by guardian without worry about cardinality explosions:
observationsReceivedByGuardianAddressTotal.WithLabelValues(their_addr.Hex()).Inc()
// []byte isn't hashable in a map. Paying a small extra cost for encoding for easier debugging.
if p.state.vaaSignatures[hash] == nil {
// We haven't yet seen this event ourselves, and therefore do not know what the VAA looks like.
// However, we have established that a valid guardian has signed it, and therefore we can
// already start aggregating signatures for it.
//
// A malicious guardian can potentially DoS this by creating fake observations at a faster rate than they decay,
// leading to a slow out-of-memory crash. We do not attempt to automatically mitigate spam attacks with valid
// signatures - such byzantine behavior would be plainly visible and would be dealt with by kicking them.
observationsUnknownTotal.Inc()
p.state.vaaSignatures[hash] = &vaaState{
firstObserved: time.Now(),
signatures: map[common.Address][]byte{},
source: "unknown",
}
}
p.state.vaaSignatures[hash].signatures[their_addr] = m.Signature
// Aggregate all valid signatures into a list of vaa.Signature and construct signed VAA.
agg := make([]bool, len(gs.Keys))
var sigs []*vaa.Signature
for i, a := range gs.Keys {
s, ok := p.state.vaaSignatures[hash].signatures[a]
if ok {
var bs [65]byte
if n := copy(bs[:], s); n != 65 {
panic(fmt.Sprintf("invalid sig len: %d", n))
}
sigs = append(sigs, &vaa.Signature{
Index: uint8(i),
Signature: bs,
})
}
agg[i] = ok
}
if p.state.vaaSignatures[hash].ourVAA != nil {
// We have seen it on chain!
// Deep copy the VAA and add signatures
v := p.state.vaaSignatures[hash].ourVAA
signed := &vaa.VAA{
Version: v.Version,
GuardianSetIndex: v.GuardianSetIndex,
Signatures: sigs,
Timestamp: v.Timestamp,
Nonce: v.Nonce,
Sequence: v.Sequence,
EmitterChain: v.EmitterChain,
EmitterAddress: v.EmitterAddress,
Payload: v.Payload,
ConsistencyLevel: v.ConsistencyLevel,
}
// report the individual signature
signatureReport := &reporter.VerifiedPeerSignature{
GuardianAddress: their_addr,
Signature: m.Signature,
EmitterChain: v.EmitterChain,
EmitterAddress: v.EmitterAddress,
Sequence: v.Sequence,
}
p.attestationEvents.ReportVerifiedPeerSignature(signatureReport)
// report the current VAAState
p.attestationEvents.ReportVAAStateUpdate(signed)
// 2/3+ majority required for VAA to be valid - wait until we have quorum to submit VAA.
quorum := CalculateQuorum(len(gs.Keys))
p.logger.Info("aggregation state for VAA",
zap.String("digest", hash),
zap.Any("set", gs.KeysAsHexStrings()),
zap.Uint32("index", gs.Index),
zap.Bools("aggregation", agg),
zap.Int("required_sigs", quorum),
zap.Int("have_sigs", len(sigs)),
)
if len(sigs) >= quorum && !p.state.vaaSignatures[hash].submitted {
vaaBytes, err := signed.Marshal()
if err != nil {
panic(err)
}
// Store signed VAA in database.
p.logger.Info("signed VAA with quorum",
zap.String("digest", hash),
zap.Any("vaa", signed),
zap.String("bytes", hex.EncodeToString(vaaBytes)),
zap.String("message_id", signed.MessageID()))
if err := p.db.StoreSignedVAA(signed); err != nil {
p.logger.Error("failed to store signed VAA", zap.Error(err))
}
p.attestationEvents.ReportVAAQuorum(signed)
p.state.vaaSignatures[hash].submitted = true
} else {
p.logger.Info("quorum not met or already submitted, doing nothing",
zap.String("digest", hash))
}
} else {
p.logger.Info("we have not yet seen this VAA - temporarily storing signature",
zap.String("digest", hash),
zap.Bools("aggregation", agg))
}
}