StoredBlock: add serializeCompactV2(), deserializeCompactV2()

The old format will soon run out of bytes for the chain work value.

Also deprecates the old methods and adds tests for the V2 format.
This commit is contained in:
Andreas Schildbach 2024-06-26 22:28:27 +02:00
parent 39f409f351
commit a1257ce83d
2 changed files with 115 additions and 17 deletions

View file

@ -40,10 +40,18 @@ import static org.bitcoinj.base.internal.Preconditions.checkState;
public class StoredBlock { public class StoredBlock {
// A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12 // A BigInteger representing the total amount of work done so far on this chain. As of June 22, 2024, it takes 12
// unsigned bytes to store this value, so we need to create an updated storage format soon. // unsigned bytes to store this value, so developers should use the V2 format.
private static final int CHAIN_WORK_BYTES = 12; private static final int CHAIN_WORK_BYTES_V1 = 12;
private static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES]; // A BigInteger representing the total amount of work done so far on this chain.
public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES + 4; // for height private static final int CHAIN_WORK_BYTES_V2 = 32;
// Height is an int.
private static final int HEIGHT_BYTES = 4;
// Used for padding.
private static final byte[] EMPTY_BYTES = new byte[CHAIN_WORK_BYTES_V2]; // fit larger format
/** Number of bytes serialized by {@link #serializeCompact(ByteBuffer)} */
public static final int COMPACT_SERIALIZED_SIZE = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V1 + HEIGHT_BYTES;
/** Number of bytes serialized by {@link #serializeCompactV2(ByteBuffer)} */
public static final int COMPACT_SERIALIZED_SIZE_V2 = Block.HEADER_SIZE + CHAIN_WORK_BYTES_V2 + HEIGHT_BYTES;
private final Block header; private final Block header;
private final BigInteger chainWork; private final BigInteger chainWork;
@ -124,16 +132,37 @@ public class StoredBlock {
return store.get(getHeader().getPrevBlockHash()); return store.get(getHeader().getPrevBlockHash());
} }
/**
* Serializes the stored block to a custom packed format. Used internally.
* As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value,
* so developers should use the V2 format.
*
* @param buffer buffer to write to
* @deprecated use {@link #serializeCompactV2(ByteBuffer)}
*/
@Deprecated
public void serializeCompact(ByteBuffer buffer) {
byte[] chainWorkBytes = ByteUtils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V1);
if (chainWorkBytes.length < CHAIN_WORK_BYTES_V1) {
// Pad to the right size.
buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V1 - chainWorkBytes.length);
}
buffer.put(chainWorkBytes);
buffer.putInt(getHeight());
byte[] bytes = getHeader().serialize();
buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions).
}
/** /**
* Serializes the stored block to a custom packed format. Used internally. * Serializes the stored block to a custom packed format. Used internally.
* *
* @param buffer buffer to write to * @param buffer buffer to write to
*/ */
public void serializeCompact(ByteBuffer buffer) { public void serializeCompactV2(ByteBuffer buffer) {
byte[] chainWorkBytes = ByteUtils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES); byte[] chainWorkBytes = ByteUtils.bigIntegerToBytes(getChainWork(), CHAIN_WORK_BYTES_V2);
if (chainWorkBytes.length < CHAIN_WORK_BYTES) { if (chainWorkBytes.length < CHAIN_WORK_BYTES_V2) {
// Pad to the right size. // Pad to the right size.
buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES - chainWorkBytes.length); buffer.put(EMPTY_BYTES, 0, CHAIN_WORK_BYTES_V2 - chainWorkBytes.length);
} }
buffer.put(chainWorkBytes); buffer.put(chainWorkBytes);
buffer.putInt(getHeight()); buffer.putInt(getHeight());
@ -141,14 +170,34 @@ public class StoredBlock {
buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions). buffer.put(bytes, 0, Block.HEADER_SIZE); // Trim the trailing 00 byte (zero transactions).
} }
/**
* Deserializes the stored block from a custom packed format. Used internally.
* As of June 22, 2024, it takes 12 unsigned bytes to store the chain work value,
* so developers should use the V2 format.
*
* @param buffer data to deserialize
* @return deserialized stored block
* @deprecated use {@link #deserializeCompactV2(ByteBuffer)}
*/
@Deprecated
public static StoredBlock deserializeCompact(ByteBuffer buffer) throws ProtocolException {
byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V1];
buffer.get(chainWorkBytes);
BigInteger chainWork = ByteUtils.bytesToBigInteger(chainWorkBytes);
int height = buffer.getInt(); // +4 bytes
byte[] header = new byte[Block.HEADER_SIZE + 1]; // Extra byte for the 00 transactions length.
buffer.get(header, 0, Block.HEADER_SIZE);
return new StoredBlock(Block.read(ByteBuffer.wrap(header)), chainWork, height);
}
/** /**
* Deserializes the stored block from a custom packed format. Used internally. * Deserializes the stored block from a custom packed format. Used internally.
* *
* @param buffer data to deserialize * @param buffer data to deserialize
* @return deserialized stored block * @return deserialized stored block
*/ */
public static StoredBlock deserializeCompact(ByteBuffer buffer) throws ProtocolException { public static StoredBlock deserializeCompactV2(ByteBuffer buffer) throws ProtocolException {
byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES]; byte[] chainWorkBytes = new byte[StoredBlock.CHAIN_WORK_BYTES_V2];
buffer.get(chainWorkBytes); buffer.get(chainWorkBytes);
BigInteger chainWork = ByteUtils.bytesToBigInteger(chainWorkBytes); BigInteger chainWork = ByteUtils.bytesToBigInteger(chainWorkBytes);
int height = buffer.getInt(); // +4 bytes int height = buffer.getInt(); // +4 bytes

View file

@ -33,9 +33,15 @@ import static org.junit.Assert.assertTrue;
public class StoredBlockTest { public class StoredBlockTest {
// Max chain work to fit in 12 bytes // Max chain work to fit in 12 bytes
private static final BigInteger MAX_WORK = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16); private static final BigInteger MAX_WORK_V1 = new BigInteger(/* 12 bytes */ "ffffffffffffffffffffffff", 16);
// Chain work too large to fit in 12 bytes // Chain work too large to fit in 12 bytes
private static final BigInteger TOO_LARGE_WORK = new BigInteger(/* 13 bytes */ "ffffffffffffffffffffffffff", 16); private static final BigInteger TOO_LARGE_WORK_V1 = new BigInteger(/* 13 bytes */ "ffffffffffffffffffffffffff", 16);
// Max chain work to fit in 32 bytes
private static final BigInteger MAX_WORK_V2 = new BigInteger(/* 32 bytes */
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16);
// Chain work too large to fit in 32 bytes
private static final BigInteger TOO_LARGE_WORK_V2 = new BigInteger(/* 33 bytes */
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16);
// Just an arbitrary block // Just an arbitrary block
private static final Block BLOCK = Block.createGenesis(Instant.now(), Block.EASIEST_DIFFICULTY_TARGET); private static final Block BLOCK = Block.createGenesis(Instant.now(), Block.EASIEST_DIFFICULTY_TARGET);
@ -44,7 +50,7 @@ public class StoredBlockTest {
new Object[] { BigInteger.ZERO }, // no work new Object[] { BigInteger.ZERO }, // no work
new Object[] { BigInteger.ONE }, // small work new Object[] { BigInteger.ONE }, // small work
new Object[] { BigInteger.valueOf(Long.MAX_VALUE) }, // a larg-ish work new Object[] { BigInteger.valueOf(Long.MAX_VALUE) }, // a larg-ish work
new Object[] { MAX_WORK }, new Object[] { MAX_WORK_V1 },
}; };
} }
@ -56,7 +62,9 @@ public class StoredBlockTest {
private Object[] vectors_serializeCompact_fail() { private Object[] vectors_serializeCompact_fail() {
return new Object[] { return new Object[] {
new Object[] { TOO_LARGE_WORK }, new Object[] { TOO_LARGE_WORK_V1 },
new Object[] { MAX_WORK_V2 },
new Object[] { TOO_LARGE_WORK_V2 },
new Object[] { BigInteger.valueOf(-1) }, // negative new Object[] { BigInteger.valueOf(-1) }, // negative
}; };
} }
@ -76,14 +84,55 @@ public class StoredBlockTest {
assertEquals(StoredBlock.deserializeCompact(buf), block); assertEquals(StoredBlock.deserializeCompact(buf), block);
} }
private Object[] vectors_serializeCompactV2_pass() {
return new Object[] {
new Object[] { BigInteger.ZERO }, // no work
new Object[] { BigInteger.ONE }, // small work
new Object[] { BigInteger.valueOf(Long.MAX_VALUE) }, // a larg-ish work
new Object[] { MAX_WORK_V1 },
new Object[] { TOO_LARGE_WORK_V1 },
new Object[] { MAX_WORK_V2 },
};
}
@Test
@Parameters(method = "vectors_serializeCompactV2_pass")
public void roundtripSerializeCompactV2_pass(BigInteger chainWork) {
roundtripSerializeCompactV2(chainWork);
}
private Object[] vectors_serializeCompactV2_fail() {
return new Object[] {
new Object[] { TOO_LARGE_WORK_V2 },
new Object[] { BigInteger.valueOf(-1) }, // negative
};
}
@Test(expected = RuntimeException.class)
@Parameters(method = "vectors_serializeCompactV2_fail")
public void roundtripSerializeCompactV2_fail(BigInteger chainWork) {
roundtripSerializeCompactV2(chainWork);
}
private void roundtripSerializeCompactV2(BigInteger chainWork) {
StoredBlock block = new StoredBlock(BLOCK, chainWork, 0);
ByteBuffer buf = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE_V2);
block.serializeCompactV2(buf);
assertEquals(StoredBlock.COMPACT_SERIALIZED_SIZE_V2, buf.position());
((Buffer) buf).rewind();
assertEquals(StoredBlock.deserializeCompactV2(buf), block);
}
@Test @Test
public void moreWorkThan() { public void moreWorkThan() {
StoredBlock noWorkBlock = new StoredBlock(BLOCK, BigInteger.ZERO, 0); StoredBlock noWorkBlock = new StoredBlock(BLOCK, BigInteger.ZERO, 0);
StoredBlock smallWorkBlock = new StoredBlock(BLOCK, BigInteger.ONE, 0); StoredBlock smallWorkBlock = new StoredBlock(BLOCK, BigInteger.ONE, 0);
StoredBlock maxWorkBlock = new StoredBlock(BLOCK, MAX_WORK, 0); StoredBlock maxWorkBlockV1 = new StoredBlock(BLOCK, MAX_WORK_V1, 0);
StoredBlock maxWorkBlockV2 = new StoredBlock(BLOCK, MAX_WORK_V2, 0);
assertTrue(smallWorkBlock.moreWorkThan(noWorkBlock)); assertTrue(smallWorkBlock.moreWorkThan(noWorkBlock));
assertTrue(maxWorkBlock.moreWorkThan(noWorkBlock)); assertTrue(maxWorkBlockV1.moreWorkThan(noWorkBlock));
assertTrue(maxWorkBlock.moreWorkThan(smallWorkBlock)); assertTrue(maxWorkBlockV1.moreWorkThan(smallWorkBlock));
assertTrue(maxWorkBlockV2.moreWorkThan(maxWorkBlockV1));
} }
} }