/* * Copyright 2014 the bitcoinj authors * * 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 com.google.common.base.Objects; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * <p>Message representing a list of unspent transaction outputs ("utxos"), returned in response to sending a * {@link GetUTXOsMessage} ("getutxos"). Note that both this message and the query that generates it are not * supported by Bitcoin Core. An implementation is available in <a href="https://github.com/bitcoinxt/bitcoinxt">Bitcoin XT</a>, * a patch set on top of Core. Thus if you want to use it, you must find some XT peers to connect to. This can be done * using a {@link org.bitcoinj.net.discovery.HttpDiscovery} class combined with an HTTP/Cartographer seed.</p> * * <p>The getutxos/utxos protocol is defined in <a href="https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki">BIP 65</a>. * In that document you can find a discussion of the security of this protocol (briefly, there is none). Because the * data found in this message is not authenticated it should be used carefully. Places where it can be useful are if * you're querying your own trusted node, if you're comparing answers from multiple nodes simultaneously and don't * believe there is a MITM on your connection, or if you're only using the returned data as a UI hint and it's OK * if the data is occasionally wrong. Bear in mind that the answer can be wrong even in the absence of malicious intent * just through the nature of querying an ever changing data source: the UTXO set may be updated by a new transaction * immediately after this message is returned.</p> * * <p>Instances of this class are not safe for use by multiple threads.</p> */ public class UTXOsMessage extends Message { private long height; private Sha256Hash chainHead; private byte[] hits; // little-endian bitset indicating whether an output was found or not. private List<TransactionOutput> outputs; private long[] heights; /** This is a special sentinel value that can appear in the heights field if the given tx is in the mempool. */ public static long MEMPOOL_HEIGHT = 0x7FFFFFFFL; public UTXOsMessage(NetworkParameters params, byte[] payloadBytes) { super(params, payloadBytes, 0); } /** * Provide an array of output objects, with nulls indicating that the output was missing. The bitset will * be calculated from this. */ public UTXOsMessage(NetworkParameters params, List<TransactionOutput> outputs, long[] heights, Sha256Hash chainHead, long height) { super(params); hits = new byte[(int) Math.ceil(outputs.size() / 8.0)]; for (int i = 0; i < outputs.size(); i++) { if (outputs.get(i) != null) Utils.setBitLE(hits, i); } this.outputs = new ArrayList<TransactionOutput>(outputs.size()); for (TransactionOutput output : outputs) { if (output != null) this.outputs.add(output); } this.chainHead = chainHead; this.height = height; this.heights = Arrays.copyOf(heights, heights.length); } @Override protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { Utils.uint32ToByteStreamLE(height, stream); stream.write(chainHead.getBytes()); stream.write(new VarInt(hits.length).encode()); stream.write(hits); stream.write(new VarInt(outputs.size()).encode()); for (int i = 0; i < outputs.size(); i++) { TransactionOutput output = outputs.get(i); Transaction tx = output.getParentTransaction(); Utils.uint32ToByteStreamLE(tx != null ? tx.getVersion() : 0L, stream); // Version Utils.uint32ToByteStreamLE(heights[i], stream); // Height output.bitcoinSerializeToStream(stream); } } @Override protected void parse() throws ProtocolException { // Format is: // uint32 chainHeight // uint256 chainHeadHash // vector<unsigned char> hitsBitmap; // vector<CCoin> outs; // // A CCoin is { int nVersion, int nHeight, CTxOut output } // The bitmap indicates which of the requested TXOs were found in the UTXO set. height = readUint32(); chainHead = readHash(); int numBytes = (int) readVarInt(); if (numBytes < 0 || numBytes > InventoryMessage.MAX_INVENTORY_ITEMS / 8) throw new ProtocolException("hitsBitmap out of range: " + numBytes); hits = readBytes(numBytes); int numOuts = (int) readVarInt(); if (numOuts < 0 || numOuts > InventoryMessage.MAX_INVENTORY_ITEMS) throw new ProtocolException("numOuts out of range: " + numOuts); outputs = new ArrayList<TransactionOutput>(numOuts); heights = new long[numOuts]; for (int i = 0; i < numOuts; i++) { long version = readUint32(); long height = readUint32(); if (version > 1) throw new ProtocolException("Unknown tx version in getutxo output: " + version); TransactionOutput output = new TransactionOutput(params, null, payload, cursor); outputs.add(output); heights[i] = height; cursor += output.length; } length = cursor; } /** * Returns a bit map indicating which of the queried outputs were found in the UTXO set. */ public byte[] getHitMap() { return Arrays.copyOf(hits, hits.length); } /** Returns the list of outputs that matched the query. */ public List<TransactionOutput> getOutputs() { return new ArrayList<TransactionOutput>(outputs); } /** Returns the block heights of each output returned in getOutputs(), or MEMPOOL_HEIGHT if not confirmed yet. */ public long[] getHeights() { return Arrays.copyOf(heights, heights.length); } @Override public String toString() { return "UTXOsMessage{" + "height=" + height + ", chainHead=" + chainHead + ", hitMap=" + Arrays.toString(hits) + ", outputs=" + outputs + ", heights=" + Arrays.toString(heights) + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UTXOsMessage other = (UTXOsMessage) o; return height == other.height && chainHead.equals(other.chainHead) && Arrays.equals(heights, other.heights) && Arrays.equals(hits, other.hits) && outputs.equals(other.outputs); } @Override public int hashCode() { return Objects.hashCode(height, chainHead, Arrays.hashCode(heights), Arrays.hashCode(hits), outputs); } }