When a $nor is rewritten as part of the change stream oplog filter and none of its children are eligible for a rewrite, an empty $nor is left in the MatchExpression tree. We do the same for $and, but in that case our subsequent call to MatchExpression::optimize when the filter is complete removes the empty $ands (and any empty $ors, though our rewrites do not produce these) from the tree. However, we don't perform any such optimization for empty $nor. Therefore, when we subsequently run the rewritten filter through parseTreeTopLevel, we end up throwing when it sees the empty $nor array.
The symptoms are worsened by the fact that we also rewrite the user $match into the buildTransactionFilter, but only a very limited set of fields. This means that even if the children of the $nor are generally eligible for rewrites (e.g. operationType), they may be excluded from this second rewrite, and thus will end up producing an empty $nor.
The bug doesn't manifest if the $nor only has a single child, because the initial optimization pass will unwrap the $nor into a $not of the child predicate, and so we never attempt to rewrite the $nor.
To fix this issue, we could perform a similar optimization-away of empty $nor as is already done for $and and $or.