From 2699ea17080efb70fee25bb0c46a029ccc2b3b39 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Tue, 4 Apr 2023 17:12:58 +0200 Subject: [PATCH] TransactionWitness: add read and write helpers * A static constructur `read()` deserializes from a payload. * A `write()` helper replaces `bitcoinSerializeToStream()`. * A `serialize()` helper gets the serialized bytes. This comes with a test. --- .../java/org/bitcoinj/core/Transaction.java | 17 +---- .../org/bitcoinj/core/TransactionWitness.java | 74 ++++++++++++++++--- .../org/bitcoinj/core/TransactionTest.java | 2 +- .../bitcoinj/core/TransactionWitnessTest.java | 35 +++++++++ 4 files changed, 103 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/org/bitcoinj/core/Transaction.java b/core/src/main/java/org/bitcoinj/core/Transaction.java index c09f690e4..fb8a7c0e9 100644 --- a/core/src/main/java/org/bitcoinj/core/Transaction.java +++ b/core/src/main/java/org/bitcoinj/core/Transaction.java @@ -706,16 +706,8 @@ public class Transaction extends Message { private void parseWitnesses(ByteBuffer payload) throws BufferUnderflowException, ProtocolException { int numWitnesses = inputs.size(); - for (int i = 0; i < numWitnesses; i++) { - VarInt pushCountVarInt = VarInt.read(payload); - int pushCount = pushCountVarInt.intValue(); - TransactionWitness witness = new TransactionWitness(pushCount); - getInput(i).setWitness(witness); - for (int y = 0; y < pushCount; y++) { - byte[] push = Buffers.readLengthPrefixedBytes(payload); - witness.setPush(y, push); - } - } + for (int i = 0; i < numWitnesses; i++) + getInput(i).setWitness(TransactionWitness.read(payload)); } /** @return true of the transaction has any witnesses in any of its inputs */ @@ -1532,9 +1524,8 @@ public class Transaction extends Message { out.bitcoinSerializeToStream(stream); // script_witnisses if (useSegwit) { - for (TransactionInput in : inputs) { - in.getWitness().bitcoinSerializeToStream(stream); - } + for (TransactionInput in : inputs) + stream.write(in.getWitness().serialize()); } // lock_time writeInt32LE(vLockTime.rawValue(), stream); diff --git a/core/src/main/java/org/bitcoinj/core/TransactionWitness.java b/core/src/main/java/org/bitcoinj/core/TransactionWitness.java index be3266c23..3b4cddf31 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionWitness.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionWitness.java @@ -15,6 +15,7 @@ package org.bitcoinj.core; import org.bitcoinj.base.VarInt; +import org.bitcoinj.base.internal.Buffers; import org.bitcoinj.base.internal.ByteUtils; import org.bitcoinj.base.internal.InternalUtils; import org.bitcoinj.crypto.ECKey; @@ -22,8 +23,9 @@ import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import javax.annotation.Nullable; -import java.io.IOException; -import java.io.OutputStream; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -60,12 +62,41 @@ public class TransactionWitness { return witness; } + /** + * Construct a transaction witness from a given list of arbitrary pushes. + * + * @param pushes list of pushes + * @return constructed transaction witness + */ + public static TransactionWitness of(List pushes) { + return new TransactionWitness(pushes); + } + + /** + * Deserialize this transaction witness 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 TransactionWitness read(ByteBuffer payload) throws BufferUnderflowException { + int pushCount = VarInt.read(payload).intValue(); + List pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); + for (int y = 0; y < pushCount; y++) + pushes.add(Buffers.readLengthPrefixedBytes(payload)); + return new TransactionWitness(pushes); + } + private final List pushes; public TransactionWitness(int pushCount) { pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); } + private TransactionWitness(List pushes) { + this.pushes = pushes; + } + public byte[] getPush(int i) { return pushes.get(i); } @@ -81,21 +112,42 @@ public class TransactionWitness { pushes.set(i, value); } - protected int getMessageSize() { + /** + * Write this transaction witness into the given buffer. + * + * @param buf buffer to write into + * @return the buffer + * @throws BufferOverflowException if the serialized data doesn't fit the remaining buffer + */ + public ByteBuffer write(ByteBuffer buf) throws BufferOverflowException { + VarInt.of(pushes.size()).write(buf); + for (byte[] push : pushes) + Buffers.writeLengthPrefixedBytes(buf, push); + return buf; + } + + /** + * Allocates a byte array and writes this transaction witness into it. + * + * @return byte array containing the transaction witness + */ + public byte[] serialize() { + return write(ByteBuffer.allocate(getMessageSize())).array(); + } + + /** + * 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 = VarInt.sizeOf(pushes.size()); for (byte[] push : pushes) size += VarInt.sizeOf(push.length) + push.length; return size; } - protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { - stream.write(VarInt.of(pushes.size()).serialize()); - for (byte[] push : pushes) { - stream.write(VarInt.of(push.length).serialize()); - stream.write(push); - } - } - @Override public String toString() { List stringPushes = new ArrayList<>(pushes.size()); diff --git a/core/src/test/java/org/bitcoinj/core/TransactionTest.java b/core/src/test/java/org/bitcoinj/core/TransactionTest.java index de7d7e8d5..d081dab5f 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionTest.java @@ -754,7 +754,7 @@ public class TransactionTest { stream.write(push); } - in.getWitness().bitcoinSerializeToStream(stream); + stream.write(in.getWitness().serialize()); } } // lock_time diff --git a/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java b/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java index 6b8895e66..15cf9ed73 100644 --- a/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java +++ b/core/src/test/java/org/bitcoinj/core/TransactionWitnessTest.java @@ -14,16 +14,28 @@ package org.bitcoinj.core; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.bitcoinj.base.internal.ByteUtils; import org.bitcoinj.crypto.ECKey; import org.bitcoinj.crypto.SignatureDecodeException; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +@RunWith(JUnitParamsRunner.class) public class TransactionWitnessTest { @Test @@ -57,4 +69,27 @@ public class TransactionWitnessTest { assertArrayEquals(signature2.encodeToBitcoin(), witness.getPush(2)); assertArrayEquals(witnessScript.getProgram(), witness.getPush(3)); } + + @Test + @Parameters(method = "randomWitness") + public void readAndWrite(TransactionWitness witness) { + ByteBuffer buf = ByteBuffer.allocate(witness.getMessageSize()); + witness.write(buf); + assertFalse(buf.hasRemaining()); + ((Buffer) buf).rewind(); + TransactionWitness witnessCopy = TransactionWitness.read(buf); + assertFalse(buf.hasRemaining()); + assertEquals(witness, witnessCopy); + } + + private Iterator randomWitness() { + Random random = new Random(); + return Stream.generate(() -> { + return TransactionWitness.of(Stream.generate(() -> { + byte[] randomBytes = new byte[random.nextInt(50)]; + random.nextBytes(randomBytes); + return randomBytes; + }).limit(random.nextInt(10)).collect(Collectors.toList())); + }).limit(10).iterator(); + } }