Update 3/30/2023

public void notify(final TransactionHashesNotification notification, final BlockNotificationContext context) { if (NotificationTrigger.Execute == context.getTrigger()) { this.transactionHashCache.putAll(notification.getPairs()); } ...
TransactionsHashesNotification is created, streamDefault is used, which expands to both outer and inner transactions:protected void notifyTransactionHashes() { final List<HashMetaDataPair> pairs = BlockExtensions.streamDefault(this.block) .map(t -> new HashMetaDataPair(HashUtils.calculateHash(t), new HashMetaData(this.block.getHeight(), t.getTimeStamp()))) .collect(Collectors.toList()); this.observer.notify(new TransactionHashesNotification(pairs)); }
BatchUniqueHashTransactionValidator checks that H(MT') is not in HashTransactionsCache, so it gets added to UnconfirmedTransactionsCache ✅public ValidationResult validate(final List<TransactionsContextPair> groupedTransactions) { ... final List<Hash> hashes = groupedTransactions.stream().flatMap(pair -> pair.getTransactions().stream()) .map(HashUtils::calculateHash).collect(Collectors.toList()); return this.validate(hashes); }
pair.getTransactions() returns  Collection<Transaction>, which only contains outer transactions. As a result, H(MT') != H(MT) is checked but H(X) is not checked.BlockUniqueHashTransactionValidator has the same logic as BatchUniqueHashTransactionValidator, so only the hash of the outer MT' is checked. As a result, the transaction is included in the harvested block ✅public ValidationResult validate(final Block ... final List<Hash> hashes = block.getTransactions().stream().map(HashUtils::calculateHash).collect(Collectors.toList()); return this.transactionHashCache.anyHashExists(hashes) ? ValidationResult.NEUTRAL : ValidationResult.SUCCESS; }
TransactionHashesObserver attempts to add both H(MT') and H(X) to HashTransactionsCache during part of the block processing that makes state changes. This fails because H(X) is already present in the UnconfirmedTransactionsCache, so the harvested block gets rejected  🛑UnconfirmedTransactionsCache
TransactionExtensions.streamDefault().TransactionExtensions.streamDefault will prevent MT' (or similar) from getting added to the `UnconfirmedTransactionsCache.public ValidationResult validate(final List<TransactionsContextPair> groupedTransactions) { if (groupedTransactions.isEmpty()) { return ValidationResult.SUCCESS; } final List<Hash> hashes = groupedTransactions.stream().flatMap(pair -> pair.getTransactions().stream()) .flatMap(transaction -> TransactionExtensions.streamDefault(transaction)).map(HashUtils::calculateHash) .collect(Collectors.toList()); return this.validate(hashes); }
TransactionExtensions.streamDefault will prevent MT' (or similar) from getting added to a newly harvested block.public ValidationResult validate(final Block block) { if (block.getTransactions().isEmpty()) { return ValidationResult.SUCCESS; } final List<Hash> hashes = block.getTransactions().stream().flatMap(transaction -> TransactionExtensions.streamDefault(transaction)) .map(HashUtils::calculateHash).collect(Collectors.toList()); return this.transactionHashCache.anyHashExists(hashes) ? ValidationResult.NEUTRAL : ValidationResult.SUCCESS; }