Soft-fail (#1364)
* Initial work on soft-fail * Fix state block retrieval * Copy-pasta QueryLatestEventsAndState code * Fix state lookup * Clean up * Fix up failing sytest * Linting * Update previous events SQLite insert query * Update SQLite InsertPreviousEvent properly * Hopefully fix the event references updates Co-authored-by: Kegan Dougal <kegan@matrix.org>main
parent
880b164490
commit
a06c18bb56
|
@ -122,15 +122,7 @@ func SendEventWithRewrite(
|
||||||
// We will handle an event as if it's an outlier if one of the
|
// We will handle an event as if it's an outlier if one of the
|
||||||
// following conditions is true:
|
// following conditions is true:
|
||||||
storeAsOutlier := false
|
storeAsOutlier := false
|
||||||
if authOrStateEvent.Type() == event.Type() && *authOrStateEvent.StateKey() == *event.StateKey() {
|
if _, ok := isCurrentState[authOrStateEvent.EventID()]; !ok {
|
||||||
// The event is a state event but the input event is going to
|
|
||||||
// replace it, therefore it can't be added to the state or we'll
|
|
||||||
// get duplicate state keys in the state block. We'll send it
|
|
||||||
// as an outlier because we don't know if something will be
|
|
||||||
// referring to it as an auth event, but need it to be stored
|
|
||||||
// just in case.
|
|
||||||
storeAsOutlier = true
|
|
||||||
} else if _, ok := isCurrentState[authOrStateEvent.EventID()]; !ok {
|
|
||||||
// The event is an auth event and isn't a part of the state set.
|
// The event is an auth event and isn't a part of the state set.
|
||||||
// We'll send it as an outlier because we need it to be stored
|
// We'll send it as an outlier because we need it to be stored
|
||||||
// in case something is referring to it as an auth event.
|
// in case something is referring to it as an auth event.
|
||||||
|
|
|
@ -16,13 +16,78 @@ package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/state"
|
||||||
"github.com/matrix-org/dendrite/roomserver/storage"
|
"github.com/matrix-org/dendrite/roomserver/storage"
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckForSoftFail returns true if the event should be soft-failed
|
||||||
|
// and false otherwise. The return error value should be checked before
|
||||||
|
// the soft-fail bool.
|
||||||
|
func CheckForSoftFail(
|
||||||
|
ctx context.Context,
|
||||||
|
db storage.Database,
|
||||||
|
event gomatrixserverlib.HeaderedEvent,
|
||||||
|
stateEventIDs []string,
|
||||||
|
) (bool, error) {
|
||||||
|
rewritesState := len(stateEventIDs) > 1
|
||||||
|
|
||||||
|
var authStateEntries []types.StateEntry
|
||||||
|
var err error
|
||||||
|
if rewritesState {
|
||||||
|
authStateEntries, err = db.StateEntriesForEventIDs(ctx, stateEventIDs)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("StateEntriesForEventIDs failed: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Work out if the room exists.
|
||||||
|
var roomInfo *types.RoomInfo
|
||||||
|
roomInfo, err = db.RoomInfo(ctx, event.RoomID())
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("db.RoomNID: %w", err)
|
||||||
|
}
|
||||||
|
if roomInfo == nil || roomInfo.IsStub {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then get the state entries for the current state snapshot.
|
||||||
|
// We'll use this to check if the event is allowed right now.
|
||||||
|
roomState := state.NewStateResolution(db, *roomInfo)
|
||||||
|
authStateEntries, err = roomState.LoadStateAtSnapshot(ctx, roomInfo.StateSnapshotNID)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("roomState.LoadStateAtSnapshot: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a special case, it's possible that the room will have no
|
||||||
|
// state because we haven't received a m.room.create event yet.
|
||||||
|
// If we're now processing the first create event then never
|
||||||
|
// soft-fail it.
|
||||||
|
if len(authStateEntries) == 0 && event.Type() == gomatrixserverlib.MRoomCreate {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work out which of the state events we actually need.
|
||||||
|
stateNeeded := gomatrixserverlib.StateNeededForAuth([]gomatrixserverlib.Event{event.Unwrap()})
|
||||||
|
|
||||||
|
// Load the actual auth events from the database.
|
||||||
|
authEvents, err := loadAuthEvents(ctx, db, stateNeeded, authStateEntries)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("loadAuthEvents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the event is allowed.
|
||||||
|
if err = gomatrixserverlib.Allowed(event.Event, &authEvents); err != nil {
|
||||||
|
// return true, nil
|
||||||
|
return true, fmt.Errorf("gomatrixserverlib.Allowed: %w", err)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CheckAuthEvents checks that the event passes authentication checks
|
// CheckAuthEvents checks that the event passes authentication checks
|
||||||
// Returns the numeric IDs for the auth events.
|
// Returns the numeric IDs for the auth events.
|
||||||
func CheckAuthEvents(
|
func CheckAuthEvents(
|
||||||
|
|
|
@ -53,6 +53,20 @@ func (r *Inputer) processRoomEvent(
|
||||||
isRejected = true
|
isRejected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var softfail bool
|
||||||
|
if input.Kind == api.KindBackfill || input.Kind == api.KindNew {
|
||||||
|
// Check that the event passes authentication checks based on the
|
||||||
|
// current room state.
|
||||||
|
softfail, err = helpers.CheckForSoftFail(ctx, r.DB, headered, input.StateEventIDs)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"event_id": event.EventID(),
|
||||||
|
"type": event.Type(),
|
||||||
|
"room": event.RoomID(),
|
||||||
|
}).WithError(err).Info("Error authing soft-failed event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we don't have a transaction ID then get one.
|
// If we don't have a transaction ID then get one.
|
||||||
if input.TransactionID != nil {
|
if input.TransactionID != nil {
|
||||||
tdID := input.TransactionID
|
tdID := input.TransactionID
|
||||||
|
@ -88,6 +102,7 @@ func (r *Inputer) processRoomEvent(
|
||||||
"event_id": event.EventID(),
|
"event_id": event.EventID(),
|
||||||
"type": event.Type(),
|
"type": event.Type(),
|
||||||
"room": event.RoomID(),
|
"room": event.RoomID(),
|
||||||
|
"sender": event.Sender(),
|
||||||
}).Debug("Stored outlier")
|
}).Debug("Stored outlier")
|
||||||
return event.EventID(), nil
|
return event.EventID(), nil
|
||||||
}
|
}
|
||||||
|
@ -110,11 +125,13 @@ func (r *Inputer) processRoomEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
// We stop here if the event is rejected: We've stored it but won't update forward extremities or notify anyone about it.
|
// We stop here if the event is rejected: We've stored it but won't update forward extremities or notify anyone about it.
|
||||||
if isRejected {
|
if isRejected || softfail {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"event_id": event.EventID(),
|
"event_id": event.EventID(),
|
||||||
"type": event.Type(),
|
"type": event.Type(),
|
||||||
"room": event.RoomID(),
|
"room": event.RoomID(),
|
||||||
|
"soft_fail": softfail,
|
||||||
|
"sender": event.Sender(),
|
||||||
}).Debug("Stored rejected event")
|
}).Debug("Stored rejected event")
|
||||||
return event.EventID(), rejectionErr
|
return event.EventID(), rejectionErr
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ package sqlite3
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/storage/shared"
|
"github.com/matrix-org/dendrite/roomserver/storage/shared"
|
||||||
|
@ -25,10 +27,15 @@ import (
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: previous_reference_sha256 was NOT NULL before but it broke sytest because
|
||||||
|
// sytest sends no SHA256 sums in the prev_events references in the soft-fail tests.
|
||||||
|
// In Postgres an empty BYTEA field is not NULL so it's fine there. In SQLite it
|
||||||
|
// seems to care that it's empty and therefore hits a NOT NULL constraint on insert.
|
||||||
|
// We should really work out what the right thing to do here is.
|
||||||
const previousEventSchema = `
|
const previousEventSchema = `
|
||||||
CREATE TABLE IF NOT EXISTS roomserver_previous_events (
|
CREATE TABLE IF NOT EXISTS roomserver_previous_events (
|
||||||
previous_event_id TEXT NOT NULL,
|
previous_event_id TEXT NOT NULL,
|
||||||
previous_reference_sha256 BLOB NOT NULL,
|
previous_reference_sha256 BLOB,
|
||||||
event_nids TEXT NOT NULL,
|
event_nids TEXT NOT NULL,
|
||||||
UNIQUE (previous_event_id, previous_reference_sha256)
|
UNIQUE (previous_event_id, previous_reference_sha256)
|
||||||
);
|
);
|
||||||
|
@ -45,6 +52,11 @@ const insertPreviousEventSQL = `
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const selectPreviousEventNIDsSQL = `
|
||||||
|
SELECT event_nids FROM roomserver_previous_events
|
||||||
|
WHERE previous_event_id = $1 AND previous_reference_sha256 = $2
|
||||||
|
`
|
||||||
|
|
||||||
// Check if the event is referenced by another event in the table.
|
// Check if the event is referenced by another event in the table.
|
||||||
// This should only be done while holding a "FOR UPDATE" lock on the row in the rooms table for this room.
|
// This should only be done while holding a "FOR UPDATE" lock on the row in the rooms table for this room.
|
||||||
const selectPreviousEventExistsSQL = `
|
const selectPreviousEventExistsSQL = `
|
||||||
|
@ -55,6 +67,7 @@ const selectPreviousEventExistsSQL = `
|
||||||
type previousEventStatements struct {
|
type previousEventStatements struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
insertPreviousEventStmt *sql.Stmt
|
insertPreviousEventStmt *sql.Stmt
|
||||||
|
selectPreviousEventNIDsStmt *sql.Stmt
|
||||||
selectPreviousEventExistsStmt *sql.Stmt
|
selectPreviousEventExistsStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +82,7 @@ func NewSqlitePrevEventsTable(db *sql.DB) (tables.PreviousEvents, error) {
|
||||||
|
|
||||||
return s, shared.StatementList{
|
return s, shared.StatementList{
|
||||||
{&s.insertPreviousEventStmt, insertPreviousEventSQL},
|
{&s.insertPreviousEventStmt, insertPreviousEventSQL},
|
||||||
|
{&s.selectPreviousEventNIDsStmt, selectPreviousEventNIDsSQL},
|
||||||
{&s.selectPreviousEventExistsStmt, selectPreviousEventExistsSQL},
|
{&s.selectPreviousEventExistsStmt, selectPreviousEventExistsSQL},
|
||||||
}.Prepare(db)
|
}.Prepare(db)
|
||||||
}
|
}
|
||||||
|
@ -80,9 +94,28 @@ func (s *previousEventStatements) InsertPreviousEvent(
|
||||||
previousEventReferenceSHA256 []byte,
|
previousEventReferenceSHA256 []byte,
|
||||||
eventNID types.EventNID,
|
eventNID types.EventNID,
|
||||||
) error {
|
) error {
|
||||||
stmt := sqlutil.TxStmt(txn, s.insertPreviousEventStmt)
|
var eventNIDs string
|
||||||
_, err := stmt.ExecContext(
|
eventNIDAsString := fmt.Sprintf("%d", eventNID)
|
||||||
ctx, previousEventID, previousEventReferenceSHA256, int64(eventNID),
|
selectStmt := sqlutil.TxStmt(txn, s.selectPreviousEventExistsStmt)
|
||||||
|
err := selectStmt.QueryRowContext(ctx, previousEventID, previousEventReferenceSHA256).Scan(&eventNIDs)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("selectStmt.QueryRowContext.Scan: %w", err)
|
||||||
|
}
|
||||||
|
var nids []string
|
||||||
|
if eventNIDs != "" {
|
||||||
|
nids = strings.Split(eventNIDs, ",")
|
||||||
|
for _, nid := range nids {
|
||||||
|
if nid == eventNIDAsString {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventNIDs = strings.Join(append(nids, eventNIDAsString), ",")
|
||||||
|
} else {
|
||||||
|
eventNIDs = eventNIDAsString
|
||||||
|
}
|
||||||
|
insertStmt := sqlutil.TxStmt(txn, s.insertPreviousEventStmt)
|
||||||
|
_, err = insertStmt.ExecContext(
|
||||||
|
ctx, previousEventID, previousEventReferenceSHA256, eventNIDs,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,4 @@ Inbound federation accepts a second soft-failed event
|
||||||
Outbound federation requests missing prev_events and then asks for /state_ids and resolves the state
|
Outbound federation requests missing prev_events and then asks for /state_ids and resolves the state
|
||||||
|
|
||||||
# We don't implement lazy membership loading yet.
|
# We don't implement lazy membership loading yet.
|
||||||
The only membership state included in a gapped incremental sync is for senders in the timeline
|
The only membership state included in a gapped incremental sync is for senders in the timeline
|
||||||
|
|
||||||
# flakey since implementing rejected events
|
|
||||||
Inbound federation correctly soft fails events
|
|
|
@ -472,4 +472,6 @@ We can't peek into rooms with joined history_visibility
|
||||||
Local users can peek by room alias
|
Local users can peek by room alias
|
||||||
Peeked rooms only turn up in the sync for the device who peeked them
|
Peeked rooms only turn up in the sync for the device who peeked them
|
||||||
Room state at a rejected message event is the same as its predecessor
|
Room state at a rejected message event is the same as its predecessor
|
||||||
Room state at a rejected state event is the same as its predecessor
|
Room state at a rejected state event is the same as its predecessor
|
||||||
|
Inbound federation correctly soft fails events
|
||||||
|
Inbound federation accepts a second soft-failed event
|
Loading…
Reference in New Issue