Transaction: when serializing a transaction re-use buffer as much as possible

This introduces a ByteBuffer-based `write()`, which calls `write()` on
TransactionInput, TransactionOutput, etc. Allocation of intermediate
buffers is avoided.

This is used for getTxId(), getWTxId() for faster id generation.

Subclass `TransactionTest.HugeDeclaredSizeTransaction` is also changed
to `write()`.
This commit is contained in:
Andreas Schildbach 2025-02-05 15:23:19 +01:00
parent 85d9d3e881
commit e20e5948dd
2 changed files with 97 additions and 49 deletions

View file

@ -53,6 +53,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.time.Instant;
@ -337,13 +338,9 @@ public class Transaction extends BaseMessage {
if (!hasWitnesses() && cachedWTxId != null) {
cachedTxId = cachedWTxId;
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
bitcoinSerializeToStream(baos, false);
} catch (IOException e) {
throw new RuntimeException(e); // cannot happen
}
cachedTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(baos.toByteArray()));
ByteBuffer buf = ByteBuffer.allocate(messageSize(false));
write(buf, false);
cachedTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(buf.array()));
}
}
return cachedTxId;
@ -368,13 +365,10 @@ public class Transaction extends BaseMessage {
if (!hasWitnesses() && cachedTxId != null) {
cachedWTxId = cachedTxId;
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
bitcoinSerializeToStream(baos, hasWitnesses());
} catch (IOException e) {
throw new RuntimeException(e); // cannot happen
}
cachedWTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(baos.toByteArray()));
boolean useWitness = hasWitnesses();
ByteBuffer buf = ByteBuffer.allocate(messageSize(useWitness));
write(buf, useWitness);
cachedWTxId = Sha256Hash.wrapReversed(Sha256Hash.hashTwice(buf.array()));
}
}
return cachedWTxId;
@ -1479,6 +1473,10 @@ public class Transaction extends BaseMessage {
@Override
public int messageSize() {
boolean useSegwit = hasWitnesses() && allowWitness(protocolVersion);
return messageSize(useSegwit);
}
protected int messageSize(boolean useSegwit) {
int size = 4; // version
if (useSegwit)
size += 2; // marker, flag
@ -1505,30 +1503,43 @@ public class Transaction extends BaseMessage {
* Serialize according to <a href="https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki">BIP144</a> or the
* <a href="https://en.bitcoin.it/wiki/Protocol_documentation#tx">classic format</a>, depending on if segwit is
* desired.
* <p>
* Write this transaction into the given buffer.
*
* @param buf buffer to write into
* @return the buffer
* @throws BufferOverflowException if the transaction doesn't fit the remaining buffer
*/
protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
protected ByteBuffer write(ByteBuffer buf, boolean useSegwit) throws BufferOverflowException {
// version
writeInt32LE(version, stream);
ByteUtils.writeInt32LE(version, buf);
// marker, flag
if (useSegwit) {
stream.write(0);
stream.write(1);
buf.put((byte) 0);
buf.put((byte) 1);
}
// txin_count, txins
stream.write(VarInt.of(inputs.size()).serialize());
VarInt.of(inputs.size()).write(buf);
for (TransactionInput in : inputs)
stream.write(in.serialize());
in.write(buf);
// txout_count, txouts
stream.write(VarInt.of(outputs.size()).serialize());
VarInt.of(outputs.size()).write(buf);
for (TransactionOutput out : outputs)
stream.write(out.serialize());
out.write(buf);
// script_witnisses
if (useSegwit) {
for (TransactionInput in : inputs)
stream.write(in.getWitness().serialize());
in.getWitness().write(buf);
}
// lock_time
writeInt32LE(vLockTime.rawValue(), stream);
ByteUtils.writeInt32LE(vLockTime.rawValue(), buf);
return buf;
}
private void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(messageSize(useSegwit));
write(buf, useSegwit);
stream.write(buf.array());
}
/**

View file

@ -42,6 +42,7 @@ import org.junit.Test;
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.time.Instant;
@ -725,41 +726,77 @@ public class TransactionTest {
}
@Override
protected void bitcoinSerializeToStream(OutputStream stream, boolean useSegwit) throws IOException {
// version
writeInt32LE(getVersion(), stream);
// marker, flag
protected int messageSize(boolean useSegwit) {
int size = 4; // version
if (useSegwit)
size += 2; // marker, flag
List<TransactionInput> inputs = getInputs();
long inputsSize = hackInputsSize ? Integer.MAX_VALUE : inputs.size();
size += VarInt.sizeOf(inputsSize);
for (TransactionInput in : inputs)
size += in.messageSize();
List<TransactionOutput> outputs = getOutputs();
long outputsSize = hackOutputsSize ? Integer.MAX_VALUE : outputs.size();
size += VarInt.sizeOf(outputsSize);
for (TransactionOutput out : outputs)
size += out.messageSize();
if (useSegwit) {
stream.write(0);
stream.write(1);
}
// txin_count, txins
long inputsSize = hackInputsSize ? Integer.MAX_VALUE : getInputs().size();
stream.write(VarInt.of(inputsSize).serialize());
for (TransactionInput in : getInputs())
stream.write(in.serialize());
// txout_count, txouts
long outputsSize = hackOutputsSize ? Integer.MAX_VALUE : getOutputs().size();
stream.write(VarInt.of(outputsSize).serialize());
for (TransactionOutput out : getOutputs())
stream.write(out.serialize());
// script_witnisses
if (useSegwit) {
for (TransactionInput in : getInputs()) {
for (TransactionInput in : inputs) {
TransactionWitness witness = in.getWitness();
long pushCount = hackWitnessPushCountSize ? Integer.MAX_VALUE : witness.getPushCount();
stream.write(VarInt.of(pushCount).serialize());
size += VarInt.sizeOf(pushCount);
for (int i = 0; i < witness.getPushCount(); i++) {
byte[] push = witness.getPush(i);
stream.write(VarInt.of(push.length).serialize());
stream.write(push);
size += VarInt.sizeOf(push.length);
size += push.length;
}
stream.write(in.getWitness().serialize());
size += witness.messageSize();
}
}
size += 4; // locktime
return size;
}
@Override
protected ByteBuffer write(ByteBuffer buf, boolean useSegwit) throws BufferOverflowException {
// version
ByteUtils.writeInt32LE(getVersion(), buf);
// marker, flag
if (useSegwit) {
buf.put((byte) 0);
buf.put((byte) 1);
}
// txin_count, txins
List<TransactionInput> inputs = getInputs();
long inputsSize = hackInputsSize ? Integer.MAX_VALUE : inputs.size();
VarInt.of(inputsSize).write(buf);
for (TransactionInput in : inputs)
in.write(buf);
// txout_count, txouts
List<TransactionOutput> outputs = getOutputs();
long outputsSize = hackOutputsSize ? Integer.MAX_VALUE : outputs.size();
VarInt.of(outputsSize).write(buf);
for (TransactionOutput out : outputs)
out.write(buf);
// script_witnisses
if (useSegwit) {
for (TransactionInput in : inputs) {
TransactionWitness witness = in.getWitness();
long pushCount = hackWitnessPushCountSize ? Integer.MAX_VALUE : witness.getPushCount();
VarInt.of(pushCount).write(buf);
for (int i = 0; i < witness.getPushCount(); i++) {
byte[] push = witness.getPush(i);
VarInt.of(push.length).write(buf);
buf.put(push);
}
witness.write(buf);
}
}
// lock_time
writeInt32LE(lockTime().rawValue(), stream);
ByteUtils.writeInt32LE(lockTime().rawValue(), buf);
return buf;
}
}