PartialMerkleTree: divorce from Message

It is never sent on its own, so it doesn't need to be a `Message`.

* Static constructor `read()` replaces the native constructor that deserialized
  from a payload.
* `write()` helper replaces `bitcoinSerializeToStream()`.
* `serialize()` and `getMessageSize()` helpers replace `bitcoinSerialize()`.

Includes a test.
This commit is contained in:
Andreas Schildbach 2023-04-10 14:52:07 +02:00
parent cc5d735eb4
commit cd75c6ab6b
4 changed files with 132 additions and 39 deletions

View File

@ -63,14 +63,14 @@ public class FilteredBlock extends Message {
header.bitcoinSerializeToStream(stream);
else
header.cloneAsHeader().bitcoinSerializeToStream(stream);
merkleTree.bitcoinSerializeToStream(stream);
stream.write(merkleTree.serialize());
}
@Override
protected void parse(ByteBuffer payload) throws BufferUnderflowException, ProtocolException {
byte[] headerBytes = Buffers.readBytes(payload, Block.HEADER_SIZE);
header = new Block(ByteBuffer.wrap(headerBytes));
merkleTree = new PartialMerkleTree(payload);
merkleTree = PartialMerkleTree.read(payload);
}
/**

View File

@ -23,8 +23,7 @@ import org.bitcoinj.base.VarInt;
import org.bitcoinj.base.internal.Buffers;
import org.bitcoinj.base.internal.ByteUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -66,7 +65,7 @@ import static org.bitcoinj.base.internal.ByteUtils.writeInt32LE;
*
* <p>Instances of this class are not safe for use by multiple threads.</p>
*/
public class PartialMerkleTree extends Message {
public class PartialMerkleTree {
// the total number of transactions in the block
private int transactionCount;
@ -75,9 +74,22 @@ public class PartialMerkleTree extends Message {
// txids and internal hashes
private List<Sha256Hash> hashes;
public PartialMerkleTree(ByteBuffer payload) throws ProtocolException {
super(payload);
/**
* Deserialize a partial merkle tree from a given payload.
*
* @param payload payload to deserialize from
* @return read message
* @throws BufferUnderflowException if the read message extends beyond the remaining bytes of the payload
*/
public static PartialMerkleTree read(ByteBuffer payload) throws BufferUnderflowException, ProtocolException {
int transactionCount = (int) ByteUtils.readUint32(payload);
int nHashes = VarInt.read(payload).intValue();
List<Sha256Hash> hashes = new ArrayList<>(Math.min(nHashes, Utils.MAX_INITIAL_ARRAY_LENGTH));
for (int i = 0; i < nHashes; i++)
hashes.add(Sha256Hash.read(payload));
byte[] matchedChildBits = Buffers.readLengthPrefixedBytes(payload);
return new PartialMerkleTree(matchedChildBits, hashes, transactionCount);
}
/**
@ -110,27 +122,43 @@ public class PartialMerkleTree extends Message {
return new PartialMerkleTree(bits, hashes, allLeafHashes.size());
}
@Override
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
writeInt32LE(transactionCount, stream);
stream.write(VarInt.of(hashes.size()).serialize());
/**
* Write this partial merkle tree into the given buffer.
*
* @param buf buffer to write into
* @return the buffer
* @throws BufferOverflowException if the partial merkle tree doesn't fit the remaining buffer
*/
public ByteBuffer write(ByteBuffer buf) throws BufferOverflowException {
writeInt32LE(transactionCount, buf);
VarInt.of(hashes.size()).write(buf);
for (Sha256Hash hash : hashes)
stream.write(hash.serialize());
stream.write(VarInt.of(matchedChildBits.length).serialize());
stream.write(matchedChildBits);
hash.write(buf);
Buffers.writeLengthPrefixedBytes(buf, matchedChildBits);
return buf;
}
@Override
protected void parse(ByteBuffer payload) throws BufferUnderflowException, ProtocolException {
transactionCount = (int) ByteUtils.readUint32(payload);
/**
* Allocates a byte array and writes this partial merkle tree into it.
*
* @return byte array containing the partial merkle tree
*/
public byte[] serialize() {
return write(ByteBuffer.allocate(getMessageSize())).array();
}
int nHashes = VarInt.read(payload).intValue();
hashes = new ArrayList<>(Math.min(nHashes, Utils.MAX_INITIAL_ARRAY_LENGTH));
for (int i = 0; i < nHashes; i++)
hashes.add(Sha256Hash.read(payload));
matchedChildBits = Buffers.readLengthPrefixedBytes(payload);
/**
* Return the size of the serialized message. Note that if the message was deserialized from a payload, this
* size can differ from the size of the original payload.
*
* @return size of the serialized message in bytes
*/
public int getMessageSize() {
int size = Integer.BYTES; // transactionCount
size += VarInt.sizeOf(hashes.size());
size += hashes.size() * Sha256Hash.LENGTH;
size += VarInt.sizeOf(matchedChildBits.length) + matchedChildBits.length;
return size;
}
// Based on CPartialMerkleTree::TraverseAndBuild in Bitcoin Core.

View File

@ -0,0 +1,62 @@
/*
* Copyright by the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.core;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.bitcoinj.base.Sha256Hash;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@RunWith(JUnitParamsRunner.class)
public class PartialMerkleTreeTest {
@Test
@Parameters(method = "randomPartialMerkleTrees")
public void readAndWrite(PartialMerkleTree pmt) {
ByteBuffer buf = ByteBuffer.allocate(pmt.getMessageSize());
pmt.write(buf);
assertFalse(buf.hasRemaining());
((Buffer) buf).rewind();
PartialMerkleTree pmtCopy = PartialMerkleTree.read(buf);
assertFalse(buf.hasRemaining());
assertEquals(pmt, pmtCopy);
}
private Iterator<PartialMerkleTree> randomPartialMerkleTrees() {
Random random = new Random();
return Stream.generate(() -> {
byte[] randomBits = new byte[random.nextInt(20)];
random.nextBytes(randomBits);
List<Sha256Hash> hashes = Stream.generate(() -> {
byte[] randomHash = new byte[Sha256Hash.LENGTH];
return Sha256Hash.wrap(randomHash);
}).limit(random.nextInt(10)).collect(Collectors.toList());
return new PartialMerkleTree(randomBits, hashes, random.nextInt(20));
}).limit(10).iterator();
}
}

View File

@ -17,13 +17,13 @@
package org.bitcoinj.core;
import com.google.common.io.BaseEncoding;
import org.bitcoinj.base.BitcoinNetwork;
import org.bitcoinj.base.Coin;
import org.bitcoinj.base.LegacyAddress;
import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.base.VarInt;
import org.bitcoinj.base.internal.Buffers;
import org.bitcoinj.base.internal.ByteUtils;
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
import org.bitcoinj.crypto.ECKey;
@ -38,9 +38,9 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@ -235,23 +235,26 @@ public class FilteredBlockAndPartialMerkleTreeTest extends TestWithPeerGroup {
hashes.add(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000002"));
hashes.add(Sha256Hash.wrap("0000000000000000000000000000000000000000000000000000000000000003"));
PartialMerkleTree pmt = new PartialMerkleTree(bits, hashes, 3) {
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
writeInt32LE(getTransactionCount(), stream);
public ByteBuffer write(ByteBuffer buf) throws BufferOverflowException {
writeInt32LE(getTransactionCount(), buf);
// Add Integer.MAX_VALUE instead of hashes.size()
stream.write(VarInt.of(Integer.MAX_VALUE).serialize());
//stream.write(VarInt.of(hashes.size()).encode());
VarInt.of(Integer.MAX_VALUE).write(buf);
for (Sha256Hash hash : hashes)
stream.write(hash.serialize());
hash.write(buf);
Buffers.writeLengthPrefixedBytes(buf, bits);
return buf;
}
stream.write(VarInt.of(bits.length).serialize());
stream.write(bits);
@Override
public int getMessageSize() {
return super.getMessageSize() + 4; // adjust for the longer VarInt
}
};
byte[] serializedPmt = pmt.bitcoinSerialize();
byte[] serializedPmt = pmt.serialize();
try {
new PartialMerkleTree(ByteBuffer.wrap(serializedPmt));
fail("We expect ProtocolException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
} catch (ProtocolException e) {
PartialMerkleTree.read(ByteBuffer.wrap(serializedPmt));
fail("We expect BufferUnderflowException with the fixed code and OutOfMemoryError with the buggy code, so this is weird");
} catch (BufferUnderflowException e) {
//Expected, do nothing
}
}