diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinBaseManager.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinBaseManager.java index 88ea4f006..8b48148cc 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinBaseManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinBaseManager.java @@ -18,6 +18,7 @@ import com.google.common.collect.Lists; import com.google.errorprone.annotations.concurrent.GuardedBy; +import org.bitcoinj.core.InventoryItem; import org.bitcoinj.core.Utils; import org.bitcoinj.utils.Threading; import org.slf4j.Logger; @@ -81,4 +82,13 @@ public CoinJoinQueue getQueueItemAndTry() { } return null; } + + public boolean alreadyHave(InventoryItem item) { + queueLock.lock(); + try { + return coinJoinQueue.stream().anyMatch(queue -> item.hash.equals(queue.getHash())); + } finally { + queueLock.unlock(); + } + } } diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientQueueManager.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientQueueManager.java index 943d626d4..a87620f4c 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientQueueManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinClientQueueManager.java @@ -17,6 +17,7 @@ import org.bitcoinj.coinjoin.utils.CoinJoinManager; import org.bitcoinj.core.Context; +import org.bitcoinj.core.InventoryItem; import org.bitcoinj.core.MasternodeSync; import org.bitcoinj.core.Peer; import org.bitcoinj.core.Sha256Hash; diff --git a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinQueue.java b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinQueue.java index 8082ca3f9..98fc904b2 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinQueue.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/CoinJoinQueue.java @@ -116,6 +116,11 @@ public Sha256Hash getSignatureHash() { } } + @Override + public Sha256Hash getHash() { + return getSignatureHash(); + } + public boolean checkSignature(BLSPublicKey pubKey) { Sha256Hash hash = getSignatureHash(); diff --git a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java index 7477db337..1c109f705 100644 --- a/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java +++ b/core/src/main/java/org/bitcoinj/coinjoin/utils/CoinJoinManager.java @@ -39,6 +39,9 @@ import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.GetDataMessage; +import org.bitcoinj.core.InventoryItem; +import org.bitcoinj.core.InventoryMessage; import org.bitcoinj.core.MasternodeAddress; import org.bitcoinj.core.MasternodeSync; import org.bitcoinj.core.Message; @@ -53,10 +56,8 @@ import org.bitcoinj.core.listeners.TransactionReceivedInBlockListener; import org.bitcoinj.evolution.Masternode; import org.bitcoinj.evolution.MasternodeMetaDataManager; -import org.bitcoinj.evolution.SimplifiedMasternodeListDiff; import org.bitcoinj.evolution.SimplifiedMasternodeListManager; import org.bitcoinj.quorums.ChainLocksHandler; -import org.bitcoinj.quorums.QuorumRotationInfo; import org.bitcoinj.utils.ContextPropagatingThreadFactory; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Wallet; @@ -68,6 +69,9 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -83,7 +87,7 @@ public class CoinJoinManager { private static final Logger log = LoggerFactory.getLogger(CoinJoinManager.class); private final ArrayList wallets = Lists.newArrayList(); - private final Context context; + private Context context; public final HashMap coinJoinClientManagers; private final CoinJoinClientQueueManager coinJoinClientQueueManager; @@ -505,7 +509,31 @@ public void processTransaction(Transaction tx) { } public final PreMessageReceivedEventListener preMessageReceivedEventListener = (peer, m) -> { - if (m instanceof CoinJoinQueue) { + if (m instanceof InventoryMessage) { + InventoryMessage inv = (InventoryMessage) m; + List items = inv.getItems(); + List dsqList = new LinkedList<>(); + for (InventoryItem item : items) { + if (item.type == InventoryItem.Type.CoinJoinQueue) { + dsqList.add(item); + } + } + Iterator it = dsqList.iterator(); + GetDataMessage getdata = new GetDataMessage(context.getParams()); + while (it.hasNext()) { + InventoryItem item = it.next(); + if(!alreadyHave(item)) { + getdata.addItem(item); + } else { + log.info("coinjoin: DSQUEUE: already has {}", item.hash); + } + } + if (!getdata.getItems().isEmpty()) { + // This will cause us to receive a bunch of block or tx messages. + log.info("coinjoin: DSQUEUE: requesting {} dsq messages", getdata.getItems().size()); + peer.sendMessage(getdata); + } + } else if (m instanceof CoinJoinQueue) { // Offload DSQueue message processing to thread pool to avoid blocking network I/O thread messageProcessingExecutor.execute(() -> { processMessage(peer, m); @@ -530,4 +558,8 @@ public void addWallet(WalletEx wallet) { public void removeWallet(WalletEx wallet) { wallets.remove(wallet); } + + protected boolean alreadyHave(InventoryItem item) { + return coinJoinClientQueueManager.alreadyHave(item); + } } diff --git a/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java b/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java index 6228b64a8..62b94260b 100644 --- a/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java +++ b/core/src/main/java/org/bitcoinj/core/BitcoinSerializer.java @@ -99,6 +99,9 @@ public class BitcoinSerializer extends MessageSerializer { names.put(InstantSendLock.class, "isdlock"); names.put(ChainLockSignature.class, "clsig"); names.put(SendHeadersMessage.class, "sendheaders"); + names.put(SendHeaders2Message.class, "sendheaders2"); + names.put(GetHeaders2Message.class, "getheaders2"); + names.put(Headers2Message.class, "headers2"); names.put(SendAddressMessageV2.class, "sendaddrv2"); names.put(GetMasternodePaymentRequestSyncMessage.class, "mnget"); names.put(AssetLockTransaction.class, "tx"); @@ -264,6 +267,12 @@ private Message makeMessage(String command, int length, byte[] payloadBytes, byt return new VersionAck(params, payloadBytes); } else if (command.equals("headers")) { return new HeadersMessage(params, payloadBytes); + } else if (command.equals("headers2")) { + return new Headers2Message(params, payloadBytes); + } else if (command.equals("getheaders2")) { + return new GetHeaders2Message(params, payloadBytes); + } else if (command.equals("sendheaders2")) { + return new SendHeaders2Message(params, payloadBytes); } else if (command.equals("alert")) { return makeAlertMessage(payloadBytes); } else if (command.equals("filterload")) { diff --git a/core/src/main/java/org/bitcoinj/core/CompressedBlockHeader.java b/core/src/main/java/org/bitcoinj/core/CompressedBlockHeader.java new file mode 100644 index 000000000..aaf3aa462 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/CompressedBlockHeader.java @@ -0,0 +1,395 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; + +public class CompressedBlockHeader extends Message { + private static final Logger log = LoggerFactory.getLogger(CompressedBlockHeader.class); + + // Bitfield masks and values + /** Mask for version bits (bits 0-2) */ + public static final int VERSION_BIT_MASK = 0x07; + /** Value indicating new version follows (0b111) */ + public static final int VERSION_BIT_NEW = 0x07; + /** Bit 3: prevBlockHash is included */ + public static final int PREV_BLOCK_HASH_BIT = 0x08; + /** Bit 4: Full 4-byte timestamp (vs 2-byte offset) */ + public static final int TIMESTAMP_FULL_BIT = 0x10; + /** Bit 5: nBits has new value (vs same as previous) */ + public static final int NBITS_NEW_BIT = 0x20; + /** Mask for reserved bits (bits 6-7) */ + public static final int RESERVED_BITS_MASK = 0xC0; + + /** Maximum signed short value for 2-byte timestamp offset */ + private static final int MAX_TIMESTAMP_OFFSET = 32767; + /** Minimum signed short value for 2-byte timestamp offset */ + private static final int MIN_TIMESTAMP_OFFSET = -32768; + + // Header fields + private int bitfield; + private long version; + private Sha256Hash prevBlockHash; + private Sha256Hash merkleRoot; + private long time; + private long difficultyTarget; + private long nonce; + + // Compression state + private boolean isFirstInBatch; + private long timestampOffset; // Used during serialization + + /** + * Creates a CompressedBlockHeader by parsing from payload bytes. + * + * @param params the network parameters + * @param payload the raw bytes + * @param offset the offset into the payload to start parsing + * @param context the compression context for decompression + * @param isFirst true if this is the first header in a batch + * @throws ProtocolException if parsing fails + */ + public CompressedBlockHeader(NetworkParameters params, byte[] payload, int offset, + CompressedHeaderContext context, boolean isFirst) + throws ProtocolException { + super(params); + this.payload = payload; + this.cursor = offset; + this.offset = offset; + this.isFirstInBatch = isFirst; + parseWithContext(context); + } + + /** + * Creates a CompressedBlockHeader from a full Block header for serialization. + * + * @param params the network parameters + * @param header the full block header to compress + * @param context the compression context + * @param isFirst true if this is the first header in a batch + */ + public CompressedBlockHeader(NetworkParameters params, Block header, + CompressedHeaderContext context, boolean isFirst) { + super(params); + this.isFirstInBatch = isFirst; + this.version = header.getVersion(); + this.prevBlockHash = header.getPrevBlockHash(); + this.merkleRoot = header.getMerkleRoot(); + this.time = header.getTimeSeconds(); + this.difficultyTarget = header.getDifficultyTarget(); + this.nonce = header.getNonce(); + computeBitfield(context); + computeLength(); + } + + @Override + protected void parse() throws ProtocolException { + // Basic parse - actual parsing is done in parseWithContext + } + + + /* last good blocik + block: + hash: 000000ed3db503b4f783c1afd086b0ecd7111909fa3fbddef7a0b7d7847560d2 + version: 536872960 (0x20000800) (BIP34, BIP66, BIP65) + previous block: 000000791c40db19eed02b1be9b2f10d63a50e6b98ff507983ca6095f990cfc4 + merkle root: d6441f36d4c5ecfa182cd5245233258e2607a1f39084fb3f8d0e6a2dec639919 + time: 1732730462 (2024-11-27T18:01:02Z) + difficulty target (nBits): 503378505 + nonce: 3220765 + + */ + /** + * Parse the compressed header using the provided context for decompression. + */ + private void parseWithContext(CompressedHeaderContext context) throws ProtocolException { + // Read bitfield (1 byte) + bitfield = readBytes(1)[0] & 0xFF; + + // Note: We intentionally ignore reserved bits (6-7) for forward compatibility. + // Future protocol versions may use these bits for new features. + + // Parse version (bits 0-2) + // Based on C++ IsVersionCompressed(): returns (versionBits != 0) + // - versionBits == 0: NOT compressed, version in stream (4 bytes) + // - versionBits 1-7: compressed, use table at index (versionBits - 1) + int versionBits = bitfield & VERSION_BIT_MASK; + if (versionBits == 0) { + // Version is in stream - read and save as most recent + version = readUint32(); + context.saveVersionAsMostRecent(version); + } else { + // Use version from table at index (versionBits - 1) + int tableIndex = versionBits - 1; + if (tableIndex < context.getVersionTableSize()) { + version = context.getVersionAt(tableIndex); + // Mark this version as most recently used (moves to front of LRU list) + context.markVersionAsMostRecent(tableIndex); + } else { + throw new ProtocolException("Invalid version table index " + tableIndex + + " (versionBits=" + versionBits + ", table size=" + + context.getVersionTableSize() + ")"); + } + } + + // Parse prevBlockHash (bit 3) + int cursorBeforePrevHash = cursor; + if ((bitfield & PREV_BLOCK_HASH_BIT) != 0) { + // Full 32-byte hash follows + prevBlockHash = readHash(); + //log.info("Read prevBlockHash from stream: {}", prevBlockHash); + } else { + // Derived from previous header's hash + prevBlockHash = context.getPreviousBlockHash(); + //log.info("Using prevBlockHash from context: {}", prevBlockHash); + } +// log.info("After prevBlockHash: cursorBefore={}, cursorAfter={}, bytesRead={}", +// cursorBeforePrevHash, cursor, cursor - cursorBeforePrevHash); + + // merkleRoot is always present (32 bytes) + int cursorBeforeMerkle = cursor; + // Log raw bytes before parsing merkleRoot +// if (log.isInfoEnabled() && cursor + 32 <= payload.length) { +// StringBuilder sb = new StringBuilder(); +// for (int i = 0; i < Math.min(40, payload.length - cursor); i++) { +// sb.append(String.format("%02x", payload[cursor + i] & 0xFF)); +// } +// log.info("Raw bytes at merkleRoot position (cursor={}): {}", cursor, sb.toString()); +// } + merkleRoot = readHash(); +// log.info("After merkleRoot: cursorBefore={}, cursorAfter={}, merkleRoot={}", +// cursorBeforeMerkle, cursor, merkleRoot); + + // Parse timestamp (bit 4) + if ((bitfield & TIMESTAMP_FULL_BIT) != 0) { + // Full 4-byte timestamp + time = readUint32(); + } else { + // 2-byte signed offset from previous timestamp + int offsetValue = readUint16(); + // Sign extend if the high bit is set (negative offset) + if ((offsetValue & 0x8000) != 0) { + offsetValue |= 0xFFFF0000; + } + time = context.getPreviousTimestamp() + offsetValue; + } + + // Parse nBits (bit 5) + if ((bitfield & NBITS_NEW_BIT) != 0) { + // Full 4-byte nBits + difficultyTarget = readUint32(); + } else { + // Same as previous + difficultyTarget = context.getPreviousNBits(); + } + + // nonce is always present (4 bytes) + nonce = readUint32(); + + // Calculate message length + length = cursor - offset; + +// log.info("Parsed compressed header: version={}, prevHash={}, time={}, nBits={}, nonce={}, bytesRead={}", +// version, prevBlockHash, time, difficultyTarget, nonce, length); +// log.info("{}", toBlock()); + } + + /** + * Compute the bitfield for serialization based on what can be compressed. + * Also updates the context's version table (LRU). + */ + private void computeBitfield(CompressedHeaderContext context) { + bitfield = 0; + + if (isFirstInBatch) { + // First header: versionBits=0 means version in stream, include all fields + bitfield = PREV_BLOCK_HASH_BIT | TIMESTAMP_FULL_BIT | NBITS_NEW_BIT; + // versionBits stays 0 (version will be written to stream) + // Save version to LRU table + context.saveVersionAsMostRecent(version); + return; + } + + // Version encoding: + // - versionBits 0: version in stream (not using table) + // - versionBits 1-7: use table at index (versionBits - 1), i.e., indices 0-6 + int versionIndex = context.getVersionIndex(version); + if (versionIndex >= 0 && versionIndex < 7) { + // Can use table: versionBits = tableIndex + 1 (1-7) + bitfield |= (versionIndex + 1); + // Mark as most recently used + context.markVersionAsMostRecent(versionIndex); + } else { + // Version not in table, versionBits stays 0 (version in stream) + context.saveVersionAsMostRecent(version); + } + + // prevBlockHash - include if not derivable from previous header hash + // Note: in a contiguous sequence, prevBlockHash should equal the hash of the previous header + // For safety, we always include it if it doesn't match + if (!prevBlockHash.equals(context.getPreviousBlockHash())) { + bitfield |= PREV_BLOCK_HASH_BIT; + } + + // Timestamp - check if 2-byte offset is sufficient + long offset = time - context.getPreviousTimestamp(); + if (offset < MIN_TIMESTAMP_OFFSET || offset > MAX_TIMESTAMP_OFFSET) { + bitfield |= TIMESTAMP_FULL_BIT; + } else { + timestampOffset = offset; + } + + // nBits - include if changed from previous + if (difficultyTarget != context.getPreviousNBits()) { + bitfield |= NBITS_NEW_BIT; + } + } + + /** + * Compute the serialized length based on the bitfield. + */ + private void computeLength() { + length = 1; // bitfield + + // Version: present only if versionBits == 0 (not compressed) + int versionBits = bitfield & VERSION_BIT_MASK; + if (versionBits == 0) { + length += 4; + } + + // prevBlockHash + if ((bitfield & PREV_BLOCK_HASH_BIT) != 0) { + length += 32; + } + + // merkleRoot (always present) + length += 32; + + // Timestamp + if ((bitfield & TIMESTAMP_FULL_BIT) != 0) { + length += 4; + } else { + length += 2; + } + + // nBits + if ((bitfield & NBITS_NEW_BIT) != 0) { + length += 4; + } + + // nonce (always present) + length += 4; + } + + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + // Write bitfield + stream.write(bitfield); + + // Version: write only if versionBits == 0 (not compressed) + int versionBits = bitfield & VERSION_BIT_MASK; + if (versionBits == 0) { + Utils.uint32ToByteStreamLE(version, stream); + } + + // prevBlockHash + if ((bitfield & PREV_BLOCK_HASH_BIT) != 0) { + stream.write(prevBlockHash.getReversedBytes()); + } + + // merkleRoot (always present) + stream.write(merkleRoot.getReversedBytes()); + + // Timestamp + if ((bitfield & TIMESTAMP_FULL_BIT) != 0) { + Utils.uint32ToByteStreamLE(time, stream); + } else { + Utils.uint16ToByteStreamLE((int) timestampOffset, stream); + } + + // nBits + if ((bitfield & NBITS_NEW_BIT) != 0) { + Utils.uint32ToByteStreamLE(difficultyTarget, stream); + } + + // nonce (always present) + Utils.uint32ToByteStreamLE(nonce, stream); + } + + /** + * Convert this compressed header to a full Block header. + * + * @return a new Block object containing the decompressed header data + */ + public Block toBlock() { + Block block = new Block(params, version, prevBlockHash, merkleRoot, + time, difficultyTarget, nonce, java.util.Collections.emptyList()); + return block.cloneAsHeader(); + } + + // Getters + + public int getBitfield() { + return bitfield; + } + + public long getVersion() { + return version; + } + + public Sha256Hash getPrevBlockHash() { + return prevBlockHash; + } + + public Sha256Hash getMerkleRoot() { + return merkleRoot; + } + + public long getTimeSeconds() { + return time; + } + + public long getDifficultyTarget() { + return difficultyTarget; + } + + public long getNonce() { + return nonce; + } + + public boolean isFirstInBatch() { + return isFirstInBatch; + } + + @Override + public String toString() { + return "CompressedBlockHeader{" + + "bitfield=0x" + Integer.toHexString(bitfield) + + ", version=" + version + + ", prevBlockHash=" + prevBlockHash + + ", merkleRoot=" + merkleRoot + + ", time=" + time + + ", difficultyTarget=" + difficultyTarget + + ", nonce=" + nonce + + ", length=" + length + + '}'; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/CompressedHeaderContext.java b/core/src/main/java/org/bitcoinj/core/CompressedHeaderContext.java new file mode 100644 index 000000000..017a49e45 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/CompressedHeaderContext.java @@ -0,0 +1,188 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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 java.util.LinkedList; + +/** + * Maintains context for compressed header encoding/decoding per DIP-0025. + * + *

This tracks: + *

    + *
  • The last 7 distinct version values in LRU order (most recent first)
  • + *
  • The previous header's hash (for prevBlockHash omission)
  • + *
  • The previous header's timestamp (for offset calculation)
  • + *
  • The previous header's nBits (for same-as-previous encoding)
  • + *
+ * + *

The version table uses LRU (Least Recently Used) ordering: + *

    + *
  • New versions are added to the front
  • + *
  • When a version is used (compressed), it moves to the front
  • + *
  • When full, the oldest version (at back) is removed
  • + *
+ * + *

Instances of this class are not safe for use by multiple threads.

+ * + * @see DIP-0025 + */ +public class CompressedHeaderContext { + + /** Maximum number of distinct versions to track */ + public static final int MAX_VERSION_TABLE_SIZE = 7; + + // LRU-ordered list: most recently used at front, oldest at back + private final LinkedList versionTable; + private Sha256Hash previousBlockHash; + private long previousTimestamp; + private long previousNBits; + + /** + * Creates a new context with empty state. + */ + public CompressedHeaderContext() { + versionTable = new LinkedList<>(); + previousBlockHash = Sha256Hash.ZERO_HASH; + previousTimestamp = 0; + previousNBits = 0; + } + + /** + * Save a version as the most recent one (add to front of LRU list). + * Called when version is read from stream (not compressed). + * Matches C++ SaveVersionAsMostRecent(). + * + * @param version the version to save + */ + public void saveVersionAsMostRecent(long version) { + // Add to front + versionTable.addFirst(version); + + // Remove oldest if over capacity + if (versionTable.size() > MAX_VERSION_TABLE_SIZE) { + versionTable.removeLast(); + } + } + + /** + * Mark an existing version as most recently used (move to front of LRU list). + * Called when version is retrieved from table (compressed). + * Matches C++ MarkVersionAsMostRecent(). + * + * @param index the current index of the version in the table + */ + public void markVersionAsMostRecent(int index) { + if (index > 0 && index < versionTable.size()) { + // Remove from current position and add to front + Long version = versionTable.remove(index); + versionTable.addFirst(version); + } + // If index == 0, it's already at the front, nothing to do + } + + /** + * Get version at the specified index in the table. + * + * @param index the index (0-6) to look up, where 0 is most recent + * @return the version value at that index + * @throws IndexOutOfBoundsException if index is out of range + */ + public long getVersionAt(int index) { + if (index < 0 || index >= versionTable.size()) { + throw new IndexOutOfBoundsException("Version index out of range: " + index + + ", table size: " + versionTable.size()); + } + return versionTable.get(index); + } + + /** + * Get the index of a version in the table. + * + * @param version the version value to look up + * @return the index (0-6) if found, or -1 if not present in the table + */ + public int getVersionIndex(long version) { + for (int i = 0; i < versionTable.size(); i++) { + if (versionTable.get(i) == version) { + return i; + } + } + return -1; + } + + /** + * @return the number of distinct versions currently in the table + */ + public int getVersionTableSize() { + return versionTable.size(); + } + + /** + * Update the previous block info after processing a header. + * Note: Version table is updated separately via saveVersionAsMostRecent/markVersionAsMostRecent. + * + * @param header the block header that was just processed + */ + public void updateAfterHeader(Block header) { + // Update previous header values for next compression/decompression + previousBlockHash = header.getHash(); + previousTimestamp = header.getTimeSeconds(); + previousNBits = header.getDifficultyTarget(); + } + + /** + * @return the hash of the previous header (for prevBlockHash omission) + */ + public Sha256Hash getPreviousBlockHash() { + return previousBlockHash; + } + + /** + * @return the timestamp of the previous header (for offset calculation) + */ + public long getPreviousTimestamp() { + return previousTimestamp; + } + + /** + * @return the nBits of the previous header (for same-as-previous encoding) + */ + public long getPreviousNBits() { + return previousNBits; + } + + /** + * Reset context to initial state. + * This should be called at the start of a new header download session. + */ + public void reset() { + versionTable.clear(); + previousBlockHash = Sha256Hash.ZERO_HASH; + previousTimestamp = 0; + previousNBits = 0; + } + + @Override + public String toString() { + return "CompressedHeaderContext{" + + "versionTableSize=" + versionTable.size() + + ", previousBlockHash=" + previousBlockHash + + ", previousTimestamp=" + previousTimestamp + + ", previousNBits=" + previousNBits + + '}'; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/GetHeaders2Message.java b/core/src/main/java/org/bitcoinj/core/GetHeaders2Message.java new file mode 100644 index 000000000..da3b67f3f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/GetHeaders2Message.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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; + +/** + * The "getheaders2" command requests compressed block headers (DIP-0025). + * It is structurally identical to "getheaders" but results in a "headers2" response + * containing compressed headers. + * + *

Instances of this class are not safe for use by multiple threads.

+ * + * @see DIP-0025 + */ +public class GetHeaders2Message extends GetBlocksMessage { + + public GetHeaders2Message(NetworkParameters params, BlockLocator locator, Sha256Hash stopHash) { + super(params, locator, stopHash); + } + + public GetHeaders2Message(NetworkParameters params, byte[] payload) throws ProtocolException { + super(params, payload); + } + + @Override + public String toString() { + return "getheaders2: " + locator.toString(); + } + + /** + * Compares two getheaders2 messages. Note that even though they are structurally identical, + * a GetHeaders2Message will not compare equal to a GetHeadersMessage or GetBlocksMessage + * containing the same data. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetHeaders2Message other = (GetHeaders2Message) o; + return version == other.version && stopHash.equals(other.stopHash) && + locator.size() == other.locator.size() && locator.equals(other.locator); + } + + @Override + public int hashCode() { + int hashCode = (int) version ^ "getheaders2".hashCode() ^ stopHash.hashCode(); + return hashCode ^= locator.hashCode(); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/Headers2Message.java b/core/src/main/java/org/bitcoinj/core/Headers2Message.java new file mode 100644 index 000000000..0ebd78ebb --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/Headers2Message.java @@ -0,0 +1,159 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A protocol message containing compressed block headers (DIP-0025). + * Sent in response to "getheaders2" command. + * + *

Format:

+ *
    + *
  • VarInt: number of headers
  • + *
  • For each header: compressed header data (variable size based on bitfield)
  • + *
+ * + *

The first header in a batch MUST include all fields (no compression). + * Subsequent headers can omit fields that can be derived from context.

+ * + *

Instances of this class are not safe for use by multiple threads.

+ * + * @see DIP-0025 + */ +public class Headers2Message extends Message { + private static final Logger log = LoggerFactory.getLogger(Headers2Message.class); + + /** Maximum number of headers in a single message (same as HeadersMessage) */ + public static final int MAX_HEADERS = 8000; + //public static final int MAX_HEADERS_8000 = 2000; + + + private List blockHeaders; + + /** + * Creates a Headers2Message by parsing from payload bytes. + * + * @param params the network parameters + * @param payload the raw message bytes + * @throws ProtocolException if parsing fails + */ + public Headers2Message(NetworkParameters params, byte[] payload) throws ProtocolException { + super(params, payload, 0); + } + + /** + * Creates a Headers2Message from block headers (for serialization). + * + * @param params the network parameters + * @param headers the block headers to include + * @throws ProtocolException if the message cannot be created + */ + public Headers2Message(NetworkParameters params, Block... headers) throws ProtocolException { + super(params); + blockHeaders = Arrays.asList(headers); + } + + /** + * Creates a Headers2Message from a list of block headers (for serialization). + * + * @param params the network parameters + * @param headers the block headers to include + * @throws ProtocolException if the message cannot be created + */ + public Headers2Message(NetworkParameters params, List headers) throws ProtocolException { + super(params); + blockHeaders = headers; + } + + @Override + public void bitcoinSerializeToStream(OutputStream stream) throws IOException { + stream.write(new VarInt(blockHeaders.size()).encode()); + + CompressedHeaderContext context = new CompressedHeaderContext(); + + for (int i = 0; i < blockHeaders.size(); i++) { + Block header = blockHeaders.get(i); + boolean isFirst = (i == 0); + + CompressedBlockHeader compressed = new CompressedBlockHeader( + params, header, context, isFirst); + compressed.bitcoinSerializeToStream(stream); + + context.updateAfterHeader(header); + } + } + + @Override + protected void parse() throws ProtocolException { + long numHeaders = readVarInt(); + log.info("Parsing headers2 message: numHeaders={}, payloadLength={}", numHeaders, payload.length); + + if (numHeaders > MAX_HEADERS) { + throw new ProtocolException("Too many headers: got " + numHeaders + + " which is larger than " + MAX_HEADERS); + } + + blockHeaders = new ArrayList<>(); + CompressedHeaderContext context = new CompressedHeaderContext(); + + for (int i = 0; i < numHeaders; i++) { + boolean isFirst = (i == 0); + +// if (i % 500 == 0 || i == numHeaders - 1) { +// log.info("Parsing header {}/{}, cursor={}, remaining={}", +// i, numHeaders, cursor, payload.length - cursor); +// } + + CompressedBlockHeader compressed = new CompressedBlockHeader( + params, payload, cursor, context, isFirst); + cursor += compressed.getMessageSize(); + + Block header = compressed.toBlock(); + blockHeaders.add(header); + + context.updateAfterHeader(header); + } + + if (length == UNKNOWN_LENGTH) { + length = cursor - offset; + } + + if (log.isDebugEnabled()) { + for (Block header : blockHeaders) { + log.debug(header.toString()); + } + } + } + + /** + * Returns the list of block headers contained in this message. + * The headers have been decompressed and are full Block objects. + * + * @return the list of block headers + */ + public List getBlockHeaders() { + return blockHeaders; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/InventoryItem.java b/core/src/main/java/org/bitcoinj/core/InventoryItem.java index d318d5659..be216f8d0 100644 --- a/core/src/main/java/org/bitcoinj/core/InventoryItem.java +++ b/core/src/main/java/org/bitcoinj/core/InventoryItem.java @@ -60,6 +60,7 @@ public enum Type { ChainLockSignature, InstantSendLock, InstantSendDeterministicLock, + CoinJoinQueue, //dsq None, } diff --git a/core/src/main/java/org/bitcoinj/core/ListMessage.java b/core/src/main/java/org/bitcoinj/core/ListMessage.java index 05302d8ab..6b32ea017 100644 --- a/core/src/main/java/org/bitcoinj/core/ListMessage.java +++ b/core/src/main/java/org/bitcoinj/core/ListMessage.java @@ -184,6 +184,9 @@ protected void parse() throws ProtocolException { case 31: type = InventoryItem.Type.InstantSendDeterministicLock; break; + case 32: + type = InventoryItem.Type.CoinJoinQueue; + break; default: type = InventoryItem.Type.None; break; diff --git a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java index 8fb66f8bf..611dad095 100644 --- a/core/src/main/java/org/bitcoinj/core/NetworkParameters.java +++ b/core/src/main/java/org/bitcoinj/core/NetworkParameters.java @@ -689,8 +689,14 @@ public static enum ProtocolVersion { MNLISTDIFF_VERSION_ORDER(70229), MNLISTDIFF_CHAINLOCKS(70230), CORE_20_1(70231), + NO_LEGACY_ISLOCK_PROTO_VERSION(70231), CORE_21(70232), - CURRENT(70232); //testnet is still 70228 + DSQ_INV_VERSION(70234), + INCREASE_MAX_HEADERS2_VERSION(70235), + // EFFICIENT_QRINFO_VERSION(70236), + // ISDLOCK_CYCLEHASH_UPDATE_VERSION(70237), + // PLATFORM_BAN_VERSION(70238), + CURRENT(70235); //testnet is still 70228 private final int bitcoinProtocol; diff --git a/core/src/main/java/org/bitcoinj/core/Peer.java b/core/src/main/java/org/bitcoinj/core/Peer.java index 86a1c89f5..eec86aacf 100644 --- a/core/src/main/java/org/bitcoinj/core/Peer.java +++ b/core/src/main/java/org/bitcoinj/core/Peer.java @@ -140,6 +140,8 @@ public class Peer extends PeerSocketHandler { @GuardedBy("lock") private boolean useFilteredBlocks = false; // The current Bloom filter set on the connection, used to tell the remote peer what transactions to send us. private volatile BloomFilter vBloomFilter; + // Whether to use compressed headers (DIP-0025) when downloading headers from this peer. + private volatile boolean useCompressedHeaders = false; // The last filtered block we received, we're waiting to fill it out with transactions. private FilteredBlock currentFilteredBlock = null; // If non-null, we should discard incoming filtered blocks because we ran out of keys and are awaiting a new filter @@ -561,7 +563,9 @@ protected void processMessage(Message m) throws Exception { // properly explore the network. processAddressMessage((AddressMessage) m); } else if (m instanceof HeadersMessage) { - processHeaders((HeadersMessage) m); + processHeaders((HeadersMessage) m, HeadersMessage.MAX_HEADERS); + } else if (m instanceof Headers2Message) { + processHeaders2((Headers2Message) m); } else if (m instanceof AlertMessage) { processAlert((AlertMessage) m); } else if (m instanceof VersionMessage) { @@ -575,6 +579,9 @@ protected void processMessage(Message m) throws Exception { } else if (m instanceof SendHeadersMessage) { // We ignore this message, because we don't announce new blocks. + } else if (m instanceof SendHeaders2Message) { + // We ignore this message, because we don't announce new blocks. + log.info("{}: Peer requested compressed header announcements (sendheaders2)", this); } else if (m instanceof SendAddressMessageV2) { // We ignore this message, because we don't reply to sendaddrv2 message. } else { @@ -721,7 +728,7 @@ protected void processAlert(AlertMessage m) { } } - protected void processHeaders(HeadersMessage m) throws ProtocolException { + protected void processHeaders(HeadersMessage m, int maxHeaders) throws ProtocolException { // Runs in network loop thread for this peer. // // This method can run if a peer just randomly sends us a "headers" message (should never happen), or more @@ -762,7 +769,7 @@ protected void processHeaders(HeadersMessage m) throws ProtocolException { StoredBlock lastHeader = headerChain.getChainHead();//new StoredBlock(previous, work, previousBlock.getHeight() + m.getBlockHeaders().size()); invokeOnHeadersDownloaded(lastHeader); log.info("processing headers till {}", lastHeader.getHeight()); - if (m.getBlockHeaders().size() < HeadersMessage.MAX_HEADERS) { + if (m.getBlockHeaders().size() < maxHeaders) { system.triggerHeadersDownloadComplete(); } else { lock.lock(); @@ -832,7 +839,7 @@ protected void processHeaders(HeadersMessage m) throws ProtocolException { } // We added all headers in the message to the chain. Request some more if we got up to the limit, otherwise // we are at the end of the chain. - if (m.getBlockHeaders().size() >= HeadersMessage.MAX_HEADERS) { + if (m.getBlockHeaders().size() >= maxHeaders) { lock.lock(); try { blockChainDownloadLocked(Sha256Hash.ZERO_HASH); @@ -848,6 +855,17 @@ protected void processHeaders(HeadersMessage m) throws ProtocolException { } } + /** + * Process compressed headers message (DIP-0025). + * The Headers2Message.parse() already decompresses headers to Block objects, + * so we can reuse the same logic as processHeaders(). + */ + protected void processHeaders2(Headers2Message m) throws ProtocolException { + // Headers2Message already decompresses to Block objects in its parse() method + HeadersMessage converted = new HeadersMessage(params, m.getBlockHeaders()); + processHeaders(converted, Headers2Message.MAX_HEADERS); + } + public void startMasternodeListDownload() { try { StoredBlock masternodeListBlock = headerChain.getChainHead().getHeight() != 0 ? @@ -1673,8 +1691,13 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { sendMessage(message); } else { // Downloading headers for a while instead of full blocks. - GetHeadersMessage message = new GetHeadersMessage(params, blockLocator, toHash); - sendMessage(message); + if (useCompressedHeaders && peerSupportsCompressedHeaders()) { + GetHeaders2Message message = new GetHeaders2Message(params, blockLocator, toHash); + sendMessage(message); + } else { + GetHeadersMessage message = new GetHeadersMessage(params, blockLocator, toHash); + sendMessage(message); + } } } @@ -1746,9 +1769,15 @@ private void blockChainHeaderDownloadLocked(Sha256Hash toHash) { lastGetHeadersBegin = chainHeadHash; lastGetHeadersEnd = toHash; - log.info("Requesting headers from : {}", this); - GetHeadersMessage message = new GetHeadersMessage(params, blockLocator, toHash); - sendMessage(message); + if (useCompressedHeaders && peerSupportsCompressedHeaders()) { + log.info("Requesting compressed headers from: {}", this); + GetHeaders2Message message = new GetHeaders2Message(params, blockLocator, toHash); + sendMessage(message); + } else { + log.info("Requesting headers from: {}", this); + GetHeadersMessage message = new GetHeadersMessage(params, blockLocator, toHash); + sendMessage(message); + } } /** @@ -2006,6 +2035,36 @@ public boolean setMinProtocolVersion(int minProtocolVersion) { return false; } + /** + * Returns true if this peer advertises support for compressed headers (DIP-0025). + * + * @see DIP-0025 + */ + public boolean peerSupportsCompressedHeaders() { + VersionMessage peerVersion = getPeerVersionMessage(); + return peerVersion != null && peerVersion.isCompressedHeadersSupported(); + } + + /** + * Sets whether to use compressed headers (DIP-0025) when downloading headers from this peer. + * Only effective if the peer advertises NODE_HEADERS_COMPRESSED support. + * + * @param use true to use compressed headers when supported + * @see DIP-0025 + */ + public void setUseCompressedHeaders(boolean use) { + this.useCompressedHeaders = use; + } + + /** + * Returns whether compressed headers are enabled for this peer. + * + * @return true if compressed headers are enabled + */ + public boolean isUseCompressedHeaders() { + return useCompressedHeaders; + } + /** *

Sets a Bloom filter on this connection. This will cause the given {@link BloomFilter} object to be sent to the * remote peer and if either a memory pool has been set using the constructor or the diff --git a/core/src/main/java/org/bitcoinj/core/PeerGroup.java b/core/src/main/java/org/bitcoinj/core/PeerGroup.java index ed27653e2..700d42f4d 100644 --- a/core/src/main/java/org/bitcoinj/core/PeerGroup.java +++ b/core/src/main/java/org/bitcoinj/core/PeerGroup.java @@ -354,6 +354,9 @@ protected int getConnectTimeoutMillis() { /** Whether bloom filter support is enabled when using a non FullPrunedBlockchain*/ private volatile boolean vBloomFilteringEnabled = true; + /** Whether to use compressed headers (DIP-0025) for header downloads */ + private volatile boolean vUseCompressedHeaders = false; + /** See {@link #PeerGroup(Context)} */ public PeerGroup(NetworkParameters params) { this(params, null); @@ -1906,6 +1909,7 @@ protected void handleNewPeer(final Peer peer) { if (shouldSendDsq) { peer.sendMessage(new SendCoinJoinQueue(params, true)); } + peer.setUseCompressedHeaders(isUseCompressedHeaders()); } finally { lock.unlock(); } @@ -3030,6 +3034,28 @@ public boolean isBloomFilteringEnabled() { return vBloomFilteringEnabled; } + /** + * Sets whether to use compressed headers (DIP-0025) for header downloads. + * When enabled, getheaders2/headers2 messages will be used instead of getheaders/headers + * when connected to peers that advertise NODE_HEADERS_COMPRESSED support. + * + * @param useCompressedHeaders true to enable compressed header downloads + * @see DIP-0025 + */ + public void setUseCompressedHeaders(boolean useCompressedHeaders) { + this.vUseCompressedHeaders = useCompressedHeaders; + } + + /** + * Returns whether compressed header downloads are enabled (DIP-0025). + * Defaults to false. + * + * @see DIP-0025 + */ + public boolean isUseCompressedHeaders() { + return vUseCompressedHeaders; + } + public void setMinRequiredProtocolVersionAndDisconnect(int protocolVersion) { setMinRequiredProtocolVersion(protocolVersion); lock.lock(); diff --git a/core/src/main/java/org/bitcoinj/core/SendHeaders2Message.java b/core/src/main/java/org/bitcoinj/core/SendHeaders2Message.java new file mode 100644 index 000000000..7d2072763 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/SendHeaders2Message.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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; + +/** + * The "sendheaders2" message indicates that a node prefers to receive new block + * announcements via compressed "headers2" messages (DIP-0025) rather than "inv" + * or uncompressed "headers" messages. + * + *

This is an empty message with no payload, similar to {@link SendHeadersMessage}.

+ * + *

Instances of this class are not safe for use by multiple threads.

+ * + * @see DIP-0025 + */ +public class SendHeaders2Message extends EmptyMessage { + + public SendHeaders2Message() { + super(); + } + + /** + * Constructor required by BitcoinSerializer for deserialization. + * + * @param params the network parameters + * @param payload the message payload (empty for this message type) + */ + public SendHeaders2Message(NetworkParameters params, byte[] payload) { + super(params); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/VersionMessage.java b/core/src/main/java/org/bitcoinj/core/VersionMessage.java index 48ef027ae..7d4a1acf5 100644 --- a/core/src/main/java/org/bitcoinj/core/VersionMessage.java +++ b/core/src/main/java/org/bitcoinj/core/VersionMessage.java @@ -68,6 +68,11 @@ public class VersionMessage extends Message { public static final int NODE_COMPACT_FILTERS = 1 << 6; /** A service bit that denotes whether the peer has at least the last two days worth of blockchain (BIP159). */ public static final int NODE_NETWORK_LIMITED = 1 << 10; + /** + * NODE_HEADERS_COMPRESSED indicates the node supports compressed headers (DIP-0025). + * @see DIP-0025 + */ + public static final int NODE_HEADERS_COMPRESSED = 1 << 11; /** * The version number of the protocol spoken. @@ -322,6 +327,14 @@ public boolean hasLimitedBlockChain() { return hasBlockChain() || (localServices & NODE_NETWORK_LIMITED) == NODE_NETWORK_LIMITED; } + /** + * Returns true if the peer supports compressed headers (DIP-0025). + * @see DIP-0025 + */ + public boolean isCompressedHeadersSupported() { + return (localServices & NODE_HEADERS_COMPRESSED) == NODE_HEADERS_COMPRESSED; + } + public static String toStringServices(long services) { List a = new LinkedList<>(); if ((services & VersionMessage.NODE_NETWORK) == VersionMessage.NODE_NETWORK) { @@ -348,6 +361,10 @@ public static String toStringServices(long services) { a.add("XTHIN"); services &= ~VersionMessage.NODE_XTHIN; } + if ((services & VersionMessage.NODE_HEADERS_COMPRESSED) == VersionMessage.NODE_HEADERS_COMPRESSED) { + a.add("HEADERS_COMPRESSED"); + services &= ~VersionMessage.NODE_HEADERS_COMPRESSED; + } if (services != 0) a.add("remaining: " + Long.toBinaryString(services)); return Joiner.on(", ").join(a); diff --git a/examples/src/main/java/org/bitcoinj/examples/DownloadHeadersBenchmark.java b/examples/src/main/java/org/bitcoinj/examples/DownloadHeadersBenchmark.java new file mode 100644 index 000000000..9130a9862 --- /dev/null +++ b/examples/src/main/java/org/bitcoinj/examples/DownloadHeadersBenchmark.java @@ -0,0 +1,203 @@ +/* + * Copyright 2026 Dash Core Group + * + * 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.examples; + +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.MasternodeSync; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.listeners.DownloadProgressTracker; +import org.bitcoinj.manager.DashSystem; +import org.bitcoinj.net.discovery.MasternodeSeedPeers; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.store.MemoryBlockStore; +import org.bitcoinj.utils.BriefLogFormatter; +import org.bitcoinj.wallet.KeyChainGroup; +import org.bitcoinj.wallet.Wallet; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * Benchmark program to compare header download times between regular headers (v1) + * and compressed headers (v2) as defined in DIP-0025. + * + * Usage: DownloadHeadersBenchmark [--debuglog] + * network: mainnet or testnet + * version: 1 (regular headers) or 2 (compressed headers) + * + * Example: + * DownloadHeadersBenchmark testnet 1 + * DownloadHeadersBenchmark mainnet 2 + */ +public class DownloadHeadersBenchmark { + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.out.println("Usage: DownloadHeadersBenchmark [--debuglog]"); + System.out.println(" network: mainnet or testnet"); + System.out.println(" version: 1 (regular headers) or 2 (compressed headers)"); + System.exit(1); + } + + // Parse arguments + String network = args[0]; + int headerVersion = Integer.parseInt(args[1]); + + if (headerVersion != 1 && headerVersion != 2) { + System.out.println("Error: version must be 1 or 2"); + System.exit(1); + } + + // Setup logging + if (args.length >= 3 && args[2].equals("--debuglog")) { + BriefLogFormatter.initVerbose(); + } else { + BriefLogFormatter.initWithSilentBitcoinJ(); + } + + // Get network parameters + NetworkParameters params; + switch (network.toLowerCase()) { + case "testnet": + params = TestNet3Params.get(); + break; + case "mainnet": + default: + params = MainNetParams.get(); + break; + } + + boolean useCompressedHeaders = (headerVersion == 2); + + System.out.println("==========================================="); + System.out.println("Header Download Benchmark"); + System.out.println("==========================================="); + System.out.println("Network: " + network); + System.out.println("Header version: " + headerVersion + " (" + + (useCompressedHeaders ? "compressed" : "regular") + ")"); + System.out.println("Start time: " + new Date()); + System.out.println("==========================================="); + + // Create a new wallet (we don't need to restore from seed) + KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params); + Wallet wallet = new Wallet(params, keyChainGroup); + DashSystem vSystem = new DashSystem(wallet.getContext()); + vSystem.initDash(true, true); + vSystem.masternodeSync.addSyncFlag(MasternodeSync.SYNC_FLAGS.SYNC_HEADERS_MN_LIST_FIRST); + vSystem.initDashSync(".", "benchmark"); + // Use in-memory block store for speed (we don't need persistence) + MemoryBlockStore blockStore = new MemoryBlockStore(params); + BlockChain chain = new BlockChain(params, blockStore); + + // Setup peer group + PeerGroup peerGroup = new PeerGroup(params, chain); + peerGroup.addPeerDiscovery(new MasternodeSeedPeers(params)); + + // Enable or disable compressed headers + peerGroup.setUseCompressedHeaders(useCompressedHeaders); + + // Add wallet to chain and peer group + chain.addWallet(wallet); + peerGroup.addWallet(wallet); + + // Track download progress + final long[] headerCount = {0}; + final long[] headerDoneTime = {0}; + final long startTime = System.currentTimeMillis(); + + DownloadProgressTracker progressTracker = new DownloadProgressTracker() { + @Override + protected void progress(double pct, int blocksSoFar, Date date) { + headerCount[0] = blocksSoFar; + if (blocksSoFar % 10000 == 0) { + long elapsed = System.currentTimeMillis() - startTime; + double headersPerSec = blocksSoFar / (elapsed / 1000.0); + System.out.printf("Progress: %.1f%% (%d headers, %.0f headers/sec)%n", + pct, blocksSoFar, headersPerSec); + } + } + + @Override + public void doneHeaderDownload() { + super.doneHeaderDownload(); + headerDoneTime[0] = System.currentTimeMillis(); + long totalTimeMs = headerDoneTime[0] - startTime;//endTime - startTime; + + // Calculate statistics + printReport(chain, totalTimeMs, headerVersion, useCompressedHeaders); + } + + @Override + public void doneDownload() { + System.out.println("Download complete!"); + } + }; + + // Start download and measure time + System.out.println("\nStarting header download...\n"); + + peerGroup.start(); + peerGroup.startBlockChainDownload(progressTracker); + + // Wait for download to complete + progressTracker.await(); + + long endTime = System.currentTimeMillis(); + long totalTimeMs = headerDoneTime[0] - startTime;//endTime - startTime; + + // Calculate statistics + printReport(chain, totalTimeMs, headerVersion, useCompressedHeaders); + + // Shutdown + peerGroup.stop(); + } + + private static void printReport(BlockChain chain, long totalTimeMs, int headerVersion, boolean useCompressedHeaders) { + long totalHeaders = chain.getBestChainHeight(); + double totalTimeSec = totalTimeMs / 1000.0; + double headersPerSec = totalHeaders / totalTimeSec; + + // Print results + System.out.println("\n==========================================="); + System.out.println("Results"); + System.out.println("==========================================="); + System.out.println("Header version: " + headerVersion + " (" + + (useCompressedHeaders ? "compressed" : "regular") + ")"); + System.out.println("Total headers: " + totalHeaders); + System.out.println("Total time: " + formatDuration(totalTimeMs)); + System.out.printf("Headers per second: %.2f%n", headersPerSec); + System.out.println("End time: " + new Date()); + System.out.println("==========================================="); + } + + private static String formatDuration(long millis) { + long hours = TimeUnit.MILLISECONDS.toHours(millis); + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis) % 60; + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis) % 60; + long ms = millis % 1000; + + if (hours > 0) { + return String.format("%d:%02d:%02d.%03d", hours, minutes, seconds, ms); + } else if (minutes > 0) { + return String.format("%d:%02d.%03d", minutes, seconds, ms); + } else { + return String.format("%d.%03d seconds", seconds, ms); + } + } +}