diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0482643d8d0..82525a3eb29 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -177,6 +177,7 @@ import org.tron.core.exception.ItemNotFoundException; import org.tron.core.exception.MaintenanceUnavailableException; import org.tron.core.exception.NonUniqueObjectException; +import org.tron.core.exception.P2pException; import org.tron.core.exception.PermissionException; import org.tron.core.exception.SignatureFormatException; import org.tron.core.exception.StoreException; @@ -606,6 +607,18 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR) .setMessage(ByteString.copyFromUtf8("Transaction expired")) .build(); + } catch (P2pException e) { + logger.info(BROADCAST_TRANS_FAILED, txID, e.getMessage()); + if (e.getType() == P2pException.TypeEnum.BAD_MESSAGE) { + // Size cap check in new TransactionMessage(byte[]) above. Map back to + // the historical TOO_BIG_TRANSACTION_ERROR response code so callers + // that branch on it keep working after the check moved up from + // Manager.validateCommon. + return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR) + .setMessage(ByteString.copyFromUtf8(e.getMessage())).build(); + } + return builder.setResult(false).setCode(response_code.OTHER_ERROR) + .setMessage(ByteString.copyFromUtf8("Error: " + e.getMessage())).build(); } catch (Exception e) { logger.warn("Broadcast transaction {} failed", txID, e); return builder.setResult(false).setCode(response_code.OTHER_ERROR) diff --git a/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java b/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java index d5aad2cd5c4..3f138f62491 100644 --- a/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java +++ b/framework/src/main/java/org/tron/core/net/message/adv/BlockMessage.java @@ -1,23 +1,44 @@ package org.tron.core.net.message.adv; +import static org.tron.core.config.Parameter.ChainConstant.BLOCK_SIZE; + +import com.google.protobuf.UnknownFieldSet; import org.tron.common.overlay.message.Message; import org.tron.common.utils.Sha256Hash; +import org.tron.core.Constant; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.exception.P2pException; +import org.tron.core.exception.P2pException.TypeEnum; import org.tron.core.net.message.MessageTypes; import org.tron.core.net.message.TronMessage; +import org.tron.protos.Protocol.Block; public class BlockMessage extends TronMessage { + // Raw byte limit on the inbound wire frame. Matches the post-parse cap + // previously enforced in BlockMsgHandler (BLOCK_SIZE + ONE_THOUSAND). + static final int MAX_RAW_SIZE = BLOCK_SIZE + Constant.ONE_THOUSAND; + private BlockCapsule block; public BlockMessage(byte[] data) throws Exception { - super(data); + if (data.length > MAX_RAW_SIZE) { + throw new P2pException(TypeEnum.BAD_MESSAGE, "block size over limit"); + } this.type = MessageTypes.BLOCK.asByte(); - this.block = new BlockCapsule(getCodedInputStream(data)); + Block parsed = Block.parseFrom(getCodedInputStream(data)); + Block sanitized = sanitize(parsed); + this.block = new BlockCapsule(sanitized); + // Reuse the input bytes only when no canonicalization happened. parseFrom + // may strip unknown fields when shouldDiscardUnknownFields is set (via + // Message.isFilter()), so check that the parsed size still equals the + // raw input length. + this.data = (sanitized == parsed && parsed.getSerializedSize() == data.length) + ? data : this.block.getData(); if (Message.isFilter()) { - Message.compareBytes(data, block.getInstance().toByteArray()); + Message.compareBytes(data, this.data); TransactionCapsule.validContractProto(block.getInstance().getTransactionsList()); } } @@ -28,6 +49,25 @@ public BlockMessage(BlockCapsule block) { this.block = block; } + private static Block sanitize(Block block) { + boolean blockHasUnknown = !block.getUnknownFields().asMap().isEmpty(); + boolean headerHasUnknown = !block.getBlockHeader().getUnknownFields().asMap().isEmpty(); + if (!blockHasUnknown && !headerHasUnknown) { + return block; + } + UnknownFieldSet empty = UnknownFieldSet.getDefaultInstance(); + Block.Builder builder = block.toBuilder(); + if (blockHasUnknown) { + builder.setUnknownFields(empty); + } + if (headerHasUnknown) { + builder.setBlockHeader(block.getBlockHeader().toBuilder() + .setUnknownFields(empty) + .build()); + } + return builder.build(); + } + public BlockId getBlockId() { return getBlockCapsule().getBlockId(); } diff --git a/framework/src/main/java/org/tron/core/net/message/adv/TransactionMessage.java b/framework/src/main/java/org/tron/core/net/message/adv/TransactionMessage.java index 3ffd65e0386..22850d4f2ae 100644 --- a/framework/src/main/java/org/tron/core/net/message/adv/TransactionMessage.java +++ b/framework/src/main/java/org/tron/core/net/message/adv/TransactionMessage.java @@ -1,8 +1,12 @@ package org.tron.core.net.message.adv; +import com.google.protobuf.UnknownFieldSet; import org.tron.common.overlay.message.Message; import org.tron.common.utils.Sha256Hash; +import org.tron.core.Constant; import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.exception.P2pException; +import org.tron.core.exception.P2pException.TypeEnum; import org.tron.core.net.message.MessageTypes; import org.tron.core.net.message.TronMessage; import org.tron.protos.Protocol.Transaction; @@ -12,12 +16,20 @@ public class TransactionMessage extends TronMessage { private TransactionCapsule transactionCapsule; public TransactionMessage(byte[] data) throws Exception { - super(data); - this.transactionCapsule = new TransactionCapsule(getCodedInputStream(data)); + Transaction parsed = Transaction.parseFrom(getCodedInputStream(data)); + checkSize(parsed); + Transaction sanitized = sanitize(parsed); + this.transactionCapsule = new TransactionCapsule(sanitized); + // Reuse the input bytes only when no canonicalization happened. parseFrom + // may also strip unknown fields when shouldDiscardUnknownFields is set + // (via Message.isFilter()), so check that the parsed size still equals + // the raw input length. + this.data = (sanitized == parsed && parsed.getSerializedSize() == data.length) + ? data : this.transactionCapsule.getData(); this.type = MessageTypes.TRX.asByte(); if (Message.isFilter()) { - compareBytes(data, transactionCapsule.getInstance().toByteArray()); - transactionCapsule + compareBytes(data, this.data); + TransactionCapsule .validContractProto(transactionCapsule.getInstance().getRawData().getContract(0)); } } @@ -28,6 +40,39 @@ public TransactionMessage(Transaction trx) { this.data = trx.toByteArray(); } + /** + * Mirror Manager.validateCommon's size check. Static helper, also invoked + * from {@link TransactionsMessage} for each tx in a batch. + */ + static void checkSize(Transaction transaction) throws P2pException { + TransactionCapsule capsule = new TransactionCapsule(transaction); + capsule.removeRedundantRet(); + long generalBytesSize = capsule.getInstance().toBuilder().clearRet().build().getSerializedSize() + + Constant.MAX_RESULT_SIZE_IN_TX + Constant.MAX_RESULT_SIZE_IN_TX; + if (generalBytesSize > Constant.TRANSACTION_MAX_BYTE_SIZE + || capsule.getData().length > Constant.TRANSACTION_MAX_BYTE_SIZE) { + throw new P2pException(TypeEnum.BAD_MESSAGE, "transaction size over limit"); + } + } + + static Transaction sanitize(Transaction transaction) { + if (transaction.getUnknownFields().asMap().isEmpty()) { + return transaction; + } + return doSanitize(transaction); + } + + /** + * Unconditional sanitize — strips Transaction top-level unknown fields. + * Callers that have already checked emptiness (e.g. {@link TransactionsMessage}) + * skip {@link #sanitize}'s fast-path check and invoke this directly. + */ + static Transaction doSanitize(Transaction transaction) { + return transaction.toBuilder() + .setUnknownFields(UnknownFieldSet.getDefaultInstance()) + .build(); + } + @Override public String toString() { return new StringBuilder().append(super.toString()) diff --git a/framework/src/main/java/org/tron/core/net/message/adv/TransactionsMessage.java b/framework/src/main/java/org/tron/core/net/message/adv/TransactionsMessage.java index 2193ac6b546..ef72eab1dec 100644 --- a/framework/src/main/java/org/tron/core/net/message/adv/TransactionsMessage.java +++ b/framework/src/main/java/org/tron/core/net/message/adv/TransactionsMessage.java @@ -1,7 +1,9 @@ package org.tron.core.net.message.adv; +import com.google.protobuf.UnknownFieldSet; import java.util.List; import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.exception.P2pException; import org.tron.core.net.message.MessageTypes; import org.tron.core.net.message.TronMessage; import org.tron.protos.Protocol; @@ -20,15 +22,42 @@ public TransactionsMessage(List trxs) { } public TransactionsMessage(byte[] data) throws Exception { - super(data); this.type = MessageTypes.TRXS.asByte(); - this.transactions = Protocol.Transactions.parseFrom(getCodedInputStream(data)); + Protocol.Transactions parsed = Protocol.Transactions.parseFrom(getCodedInputStream(data)); + this.transactions = sanitize(parsed); + // Reuse the input bytes only when no canonicalization happened. parseFrom + // may strip unknown fields when shouldDiscardUnknownFields is set (via + // Message.isFilter()), so check that the parsed size still equals the + // raw input length. + this.data = (this.transactions == parsed && parsed.getSerializedSize() == data.length) + ? data : this.transactions.toByteArray(); if (isFilter()) { - compareBytes(data, transactions.toByteArray()); + compareBytes(data, this.data); TransactionCapsule.validContractProto(transactions.getTransactionsList()); } } + private static Protocol.Transactions sanitize(Protocol.Transactions raw) + throws P2pException { + Protocol.Transactions.Builder builder = null; + if (!raw.getUnknownFields().asMap().isEmpty()) { + builder = raw.toBuilder(); + builder.setUnknownFields(UnknownFieldSet.getDefaultInstance()); + } + + for (int i = 0; i < raw.getTransactionsCount(); i++) { + Transaction tx = raw.getTransactions(i); + TransactionMessage.checkSize(tx); + if (!tx.getUnknownFields().asMap().isEmpty()) { + if (builder == null) { + builder = raw.toBuilder(); + } + builder.setTransactions(i, TransactionMessage.doSanitize(tx)); + } + } + return builder == null ? raw : builder.build(); + } + public Protocol.Transactions getTransactions() { return transactions; } diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java index 3b9e86d4791..8974edbe1ee 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java @@ -1,7 +1,6 @@ package org.tron.core.net.messagehandler; import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; -import static org.tron.core.config.Parameter.ChainConstant.BLOCK_SIZE; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; @@ -9,7 +8,6 @@ import org.springframework.stereotype.Component; import org.tron.common.prometheus.MetricKeys; import org.tron.common.prometheus.Metrics; -import org.tron.core.Constant; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Args; @@ -51,8 +49,6 @@ public class BlockMsgHandler implements TronMsgHandler { @Autowired private WitnessProductBlockService witnessProductBlockService; - private int maxBlockSize = BLOCK_SIZE + Constant.ONE_THOUSAND; - private boolean fastForward = Args.getInstance().isFastForward(); @Override @@ -62,11 +58,6 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep BlockId blockId = blockMessage.getBlockId(); BlockCapsule blockCapsule = blockMessage.getBlockCapsule(); - if (blockCapsule.getInstance().getSerializedSize() > maxBlockSize) { - logger.error("Receive bad block {} from peer {}, block size over limit", - blockMessage.getBlockId(), peer.getInetSocketAddress()); - throw new P2pException(TypeEnum.BAD_MESSAGE, "block size over limit"); - } long gap = blockCapsule.getTimeStamp() - System.currentTimeMillis(); if (gap >= BLOCK_PRODUCED_INTERVAL) { logger.error("Receive bad block {} from peer {}, block time error", diff --git a/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java new file mode 100644 index 00000000000..64063e682f0 --- /dev/null +++ b/framework/src/test/java/org/tron/core/net/message/adv/SanitizeUnknownFieldsTest.java @@ -0,0 +1,381 @@ +package org.tron.core.net.message.adv; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.UnknownFieldSet; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.common.overlay.message.Message; +import org.tron.core.Constant; +import org.tron.core.exception.P2pException; +import org.tron.core.exception.P2pException.TypeEnum; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.Protocol.Transactions; +import org.tron.protos.contract.BalanceContract.TransferContract; + +/** + * Verifies that the three P2P message constructors that take raw bytes + * (BlockMessage, TransactionMessage, TransactionsMessage) strip unknown + * protobuf fields at positions not covered by any consensus hash, while + * leaving the signed regions byte-identical so transaction id and block hash + * remain stable. + */ +public class SanitizeUnknownFieldsTest { + + /** + * 1 KiB length-delimited unknown field at tag 99999, large enough that any + * sanitize-stripped message is noticeably shorter than the padded input. + */ + private static final UnknownFieldSet PADDING = UnknownFieldSet.newBuilder() + .addField(99999, UnknownFieldSet.Field.newBuilder() + .addLengthDelimited(ByteString.copyFrom(new byte[1024])) + .build()) + .build(); + + @BeforeClass + public static void setUp() { + // Mock dynamicPropertiesStore so Message.isFilter() returns false + // (mock's primitive-long getter returns 0L by default). + Message.setDynamicPropertiesStore(Mockito.mock(DynamicPropertiesStore.class)); + } + + @After + public void resetFilter() { + // Filter-mode tests flip isFilter() on; restore the default so unrelated + // tests do not see leaked state if execution order changes. + Message.setDynamicPropertiesStore(Mockito.mock(DynamicPropertiesStore.class)); + } + + private static void enableFilter() { + DynamicPropertiesStore store = Mockito.mock(DynamicPropertiesStore.class); + Mockito.when(store.getAllowProtoFilterNum()).thenReturn(1L); + Message.setDynamicPropertiesStore(store); + } + + private static Transaction transactionWithTransferContract() { + TransferContract transfer = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(new byte[21])) + .setToAddress(ByteString.copyFrom(new byte[21])) + .setAmount(1) + .build(); + Transaction.Contract contract = Transaction.Contract.newBuilder() + .setType(ContractType.TransferContract) + .setParameter(Any.pack(transfer)) + .build(); + return Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder() + .setTimestamp(123456789L) + .addContract(contract) + .build()) + .build(); + } + + private static BlockHeader.raw sampleRawHeader() { + return BlockHeader.raw.newBuilder() + .setNumber(100) + .setTimestamp(123456789L) + .build(); + } + + private static Block sampleBlock() { + return Block.newBuilder() + .setBlockHeader(BlockHeader.newBuilder().setRawData(sampleRawHeader()).build()) + .build(); + } + + private static Transaction sampleTransaction() { + return Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(123456789L).build()) + .build(); + } + + private static UnknownFieldSet paddingOfSize(int payloadSize) { + return UnknownFieldSet.newBuilder() + .addField(99999, UnknownFieldSet.Field.newBuilder() + .addLengthDelimited(ByteString.copyFrom(new byte[payloadSize])) + .build()) + .build(); + } + + // ---- BlockMessage ---- + + @Test + public void testBlockMessageStripsBlockLevelUnknownFields() throws Exception { + Block padded = sampleBlock().toBuilder().setUnknownFields(PADDING).build(); + byte[] paddedBytes = padded.toByteArray(); + + BlockMessage msg = new BlockMessage(paddedBytes); + + assertTrue("Block-level unknown fields should be stripped", + msg.getBlockCapsule().getInstance().getUnknownFields().asMap().isEmpty()); + assertTrue("msg.data should be canonical (no padding)", + msg.getData().length < paddedBytes.length); + } + + @Test + public void testBlockMessageStripsBlockHeaderOuterUnknownFields() throws Exception { + BlockHeader paddedHeader = BlockHeader.newBuilder() + .setRawData(sampleRawHeader()) + .setUnknownFields(PADDING) + .build(); + Block padded = Block.newBuilder().setBlockHeader(paddedHeader).build(); + byte[] paddedBytes = padded.toByteArray(); + + BlockMessage msg = new BlockMessage(paddedBytes); + + assertTrue("BlockHeader outer unknown fields should be stripped", + msg.getBlockCapsule().getInstance().getBlockHeader() + .getUnknownFields().asMap().isEmpty()); + assertTrue(msg.getData().length < paddedBytes.length); + } + + @Test + public void testBlockMessagePreservesBlockHeaderRawData() throws Exception { + Block clean = sampleBlock(); + Block padded = clean.toBuilder().setUnknownFields(PADDING).build(); + + BlockMessage msg = new BlockMessage(padded.toByteArray()); + + assertEquals("BlockHeader.raw_data must be byte-identical so block hash matches", + clean.getBlockHeader().getRawData(), + msg.getBlockCapsule().getInstance().getBlockHeader().getRawData()); + } + + @Test + public void testBlockMessageCleanBlockPassesThroughUnchanged() throws Exception { + Block clean = sampleBlock(); + byte[] cleanBytes = clean.toByteArray(); + + BlockMessage msg = new BlockMessage(cleanBytes); + + assertArrayEquals("Clean block bytes should pass through unchanged", + cleanBytes, msg.getData()); + } + + // ---- TransactionMessage ---- + + @Test + public void testTransactionMessageStripsTopLevelUnknownFields() throws Exception { + Transaction padded = sampleTransaction().toBuilder().setUnknownFields(PADDING).build(); + byte[] paddedBytes = padded.toByteArray(); + + TransactionMessage msg = new TransactionMessage(paddedBytes); + + assertTrue("Transaction-level unknown fields should be stripped", + msg.getTransactionCapsule().getInstance().getUnknownFields().asMap().isEmpty()); + assertTrue(msg.getData().length < paddedBytes.length); + } + + @Test + public void testTransactionMessagePreservesTransactionId() throws Exception { + Transaction clean = sampleTransaction(); + Transaction padded = clean.toBuilder().setUnknownFields(PADDING).build(); + + TransactionMessage cleanMsg = new TransactionMessage(clean.toByteArray()); + TransactionMessage paddedMsg = new TransactionMessage(padded.toByteArray()); + + assertEquals("Padding outside raw_data must not change tx id", + cleanMsg.getTransactionCapsule().getTransactionId(), + paddedMsg.getTransactionCapsule().getTransactionId()); + } + + @Test + public void testTransactionMessageCleanTransactionPassesThroughUnchanged() throws Exception { + Transaction clean = sampleTransaction(); + byte[] cleanBytes = clean.toByteArray(); + + TransactionMessage msg = new TransactionMessage(cleanBytes); + + assertArrayEquals(cleanBytes, msg.getData()); + } + + // ---- TransactionsMessage ---- + + @Test + public void testTransactionsMessageStripsWrapperUnknownFields() throws Exception { + Transactions padded = Transactions.newBuilder() + .addTransactions(sampleTransaction()) + .setUnknownFields(PADDING) + .build(); + byte[] paddedBytes = padded.toByteArray(); + + TransactionsMessage msg = new TransactionsMessage(paddedBytes); + + assertTrue("Wrapper unknown fields should be stripped", + msg.getTransactions().getUnknownFields().asMap().isEmpty()); + assertTrue(msg.getData().length < paddedBytes.length); + } + + @Test + public void testTransactionsMessageStripsNestedTxUnknownFields() throws Exception { + Transaction paddedTx = sampleTransaction().toBuilder().setUnknownFields(PADDING).build(); + Transactions wrapper = Transactions.newBuilder().addTransactions(paddedTx).build(); + byte[] paddedBytes = wrapper.toByteArray(); + + TransactionsMessage msg = new TransactionsMessage(paddedBytes); + + assertTrue("Nested tx unknown fields should be stripped", + msg.getTransactions().getTransactions(0) + .getUnknownFields().asMap().isEmpty()); + assertTrue(msg.getData().length < paddedBytes.length); + } + + @Test + public void testTransactionsMessageCleanWrapperPassesThroughUnchanged() throws Exception { + Transactions clean = Transactions.newBuilder() + .addTransactions(sampleTransaction()) + .build(); + byte[] cleanBytes = clean.toByteArray(); + + TransactionsMessage msg = new TransactionsMessage(cleanBytes); + + assertArrayEquals(cleanBytes, msg.getData()); + } + + // ---- size rejection ---- + + @Test + public void testBlockMessageRejectsOversizedRawBytes() { + // Pad past the MAX_RAW_SIZE cap so the constructor must reject before + // it ever calls Block.parseFrom on the multi-megabyte buffer. + byte[] paddedBytes = sampleBlock().toBuilder() + .setUnknownFields(paddingOfSize(BlockMessage.MAX_RAW_SIZE)) + .build() + .toByteArray(); + assertTrue(paddedBytes.length > BlockMessage.MAX_RAW_SIZE); + + P2pException ex = assertThrows(P2pException.class, + () -> new BlockMessage(paddedBytes)); + assertEquals(TypeEnum.BAD_MESSAGE, ex.getType()); + } + + @Test + public void testTransactionMessageRejectsOversizedRawBytes() { + byte[] paddedBytes = sampleTransaction().toBuilder() + .setUnknownFields(paddingOfSize((int) Constant.TRANSACTION_MAX_BYTE_SIZE)) + .build() + .toByteArray(); + assertTrue(paddedBytes.length > Constant.TRANSACTION_MAX_BYTE_SIZE); + + P2pException ex = assertThrows(P2pException.class, + () -> new TransactionMessage(paddedBytes)); + assertEquals(TypeEnum.BAD_MESSAGE, ex.getType()); + } + + @Test + public void testTransactionsMessageRejectsOversizedNestedTx() { + // A single padded tx inside the batch should be rejected by checkSize. + Transaction oversizedTx = sampleTransaction().toBuilder() + .setUnknownFields(paddingOfSize((int) Constant.TRANSACTION_MAX_BYTE_SIZE)) + .build(); + assertTrue(oversizedTx.getSerializedSize() > Constant.TRANSACTION_MAX_BYTE_SIZE); + byte[] wrapperBytes = Transactions.newBuilder() + .addTransactions(oversizedTx) + .build() + .toByteArray(); + + P2pException ex = assertThrows(P2pException.class, + () -> new TransactionsMessage(wrapperBytes)); + assertEquals(TypeEnum.BAD_MESSAGE, ex.getType()); + } + + @Test + public void testTransactionsMessageAcceptsBatchWithinLimits() throws Exception { + byte[] bytes = Transactions.newBuilder() + .addTransactions(sampleTransaction()) + .addTransactions(sampleTransaction()) + .addTransactions(sampleTransaction()) + .build() + .toByteArray(); + + TransactionsMessage msg = new TransactionsMessage(bytes); + + assertEquals(3, msg.getTransactions().getTransactionsCount()); + } + + // ---- isFilter() == true branch (hypothetical, proposal #24 inactive on mainnet) ---- + + @Test + public void testBlockMessageRejectsPaddingWhenFilterOn() { + enableFilter(); + // Under filter mode parseFrom discards unknown fields, so the parsed + // bytes are shorter than the padded input and compareBytes rejects it. + byte[] paddedBytes = sampleBlock().toBuilder() + .setUnknownFields(PADDING) + .build() + .toByteArray(); + + P2pException ex = assertThrows(P2pException.class, + () -> new BlockMessage(paddedBytes)); + assertEquals(TypeEnum.PROTOBUF_ERROR, ex.getType()); + } + + @Test + public void testTransactionMessageValidatesContractWhenFilterOn() { + enableFilter(); + // Clean transaction with a TransferContract: compareBytes passes (raw + // equals sanitized), then validContractProto is invoked. TransactionFactory + // is not initialized in this lightweight test context, so the call throws + // PROTOBUF_ERROR. The point is to exercise the filter-mode branch so its + // statements are covered. + Transaction tx = transactionWithTransferContract(); + + P2pException ex = assertThrows(P2pException.class, + () -> new TransactionMessage(tx.toByteArray())); + assertEquals(TypeEnum.PROTOBUF_ERROR, ex.getType()); + } + + @Test + public void testTransactionsMessageRejectsPaddingWhenFilterOn() { + enableFilter(); + byte[] paddedBytes = Transactions.newBuilder() + .addTransactions(sampleTransaction()) + .setUnknownFields(PADDING) + .build() + .toByteArray(); + + P2pException ex = assertThrows(P2pException.class, + () -> new TransactionsMessage(paddedBytes)); + assertEquals(TypeEnum.PROTOBUF_ERROR, ex.getType()); + } + + @Test + public void testBlockMessageReachesFilterBranchWithCleanBlock() throws Exception { + enableFilter(); + // Clean block: compareBytes returns normally (raw == sanitized), then + // validContractProto runs on the empty transactions list and returns. + byte[] cleanBytes = sampleBlock().toByteArray(); + + BlockMessage msg = new BlockMessage(cleanBytes); + + assertEquals(100, msg.getBlockCapsule().getNum()); + } + + @Test + public void testTransactionsMessageReachesFilterBranchWithCleanWrapper() { + enableFilter(); + // Clean wrapper carrying sampleTransaction (no contracts): compareBytes + // returns normally, then validContractProto throws PROTOBUF_ERROR because + // the transaction has no contract to inspect. The point is to exercise + // the filter branch's compareBytes path past the call site. + byte[] cleanBytes = Transactions.newBuilder() + .addTransactions(sampleTransaction()) + .build() + .toByteArray(); + + P2pException ex = assertThrows(P2pException.class, + () -> new TransactionsMessage(cleanBytes)); + assertEquals(TypeEnum.PROTOBUF_ERROR, ex.getType()); + } +}