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.
This commit is contained in:
Andreas Schildbach 2023-04-04 17:12:58 +02:00
parent 41f684523d
commit 2699ea1708
4 changed files with 103 additions and 25 deletions

View File

@ -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);

View File

@ -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<byte[]> 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<byte[]> 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<byte[]> pushes;
public TransactionWitness(int pushCount) {
pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH));
}
private TransactionWitness(List<byte[]> 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<String> stringPushes = new ArrayList<>(pushes.size());

View File

@ -754,7 +754,7 @@ public class TransactionTest {
stream.write(push);
}
in.getWitness().bitcoinSerializeToStream(stream);
stream.write(in.getWitness().serialize());
}
}
// lock_time

View File

@ -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<TransactionWitness> 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();
}
}