package org.ripple.power.txns.btc; import java.io.EOFException; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import org.ripple.power.Helper; /** * <p>A block is composed of one or more transactions. The first transaction is called the coinbase transaction * and it assigns the block reward to the miner who solved the block hash. The remaining transactions move coins * from Input A to Output B. A single transaction can contain multiple inputs and multiple outputs. The sum of * the inputs minus the sum of the output represents the mining fee for that transaction.</p> * * <p>Each transaction input is connected to the output of a proceeding transaction. The input contains the * first half of a script (ScriptSig) and the output contains the second half (ScriptPubKey). The script * is interpreted to determines if the transaction input is allowed to spend the transaction output.</p> * * <p>A transaction has the following format:</p> * <pre> * Size Field Description * ==== ===== =========== * 4 bytes Version Currently 1 * VarInt InputCount Number of inputs * Variable InputList Inputs * VarInt OutputCount Number of outputs * Variable OutputList Outputs * 4 bytes LockTime Transaction lock time * </pre> * * <p>All numbers are encoded in little-endian format (least-significant byte to most-significant byte)</p> */ public class Transaction implements ByteSerializable { /** Serialized transaction data */ private final byte[] txData; /** Transaction version */ private final int txVersion; /** Transaction hash */ private final Sha256Hash txHash; /** Normalized transaction ID */ private final Sha256Hash normID; /** Transaction lock time */ private final long txLockTime; /* This a coinbase transaction */ private final boolean coinBase; /** List of transaction inputs */ private final List<TransactionInput> txInputs; /** List of transaction outputs */ private final List<TransactionOutput> txOutputs; /** * Creates a new transaction using the provided inputs * * @param inputs List of signed inputs * @param outputs List of outputs * @throws ECException Unable to sign transaction * @throws ScriptException Script processing error * @throws VerificationException Transaction verification failure */ public Transaction(List<SignedInput> inputs, List<TransactionOutput> outputs) throws ECException, ScriptException, VerificationException { SerializedBuffer outBuffer = new SerializedBuffer(1024); txVersion = 1; txOutputs = outputs; txLockTime = 0; coinBase = false; // // Create the transaction inputs // txInputs = new ArrayList<>(inputs.size()); for (int i=0; i<inputs.size(); i++) txInputs.add(new TransactionInput(this, i, inputs.get(i).getOutPoint())); // // Now sign each input and create the input scripts // for (int i=0; i<inputs.size(); i++) { SignedInput input = inputs.get(i); ECKey key = input.getKey(); byte[] contents; // // Serialize the transaction for signing using the SIGHASH_ALL hash type // outBuffer.rewind(); serializeForSignature(i, ScriptOpCodes.SIGHASH_ALL, input.getScriptBytes(), outBuffer); outBuffer.putInt(ScriptOpCodes.SIGHASH_ALL); contents = outBuffer.toByteArray(); // // Create the DER-encoded signature // ECDSASignature sig = key.createSignature(contents); byte[] encodedSig = sig.encodeToDER(); // // Create the input script using the SIGHASH_ALL hash type // <sig> <pubKey> // byte[] pubKey = key.getPubKey(); byte[] scriptBytes = new byte[1+encodedSig.length+1+1+pubKey.length]; scriptBytes[0] = (byte)(encodedSig.length+1); System.arraycopy(encodedSig, 0, scriptBytes, 1, encodedSig.length); int offset = encodedSig.length+1; scriptBytes[offset++] = (byte)ScriptOpCodes.SIGHASH_ALL; scriptBytes[offset++] = (byte)pubKey.length; System.arraycopy(pubKey, 0, scriptBytes, offset, pubKey.length); txInputs.get(i).setScriptBytes(scriptBytes); } // // Serialize the entire transaction // outBuffer.rewind(); getBytes(outBuffer); txData = outBuffer.toByteArray(); // // Calculate the transaction hash using the serialized data // txHash = new Sha256Hash(Helper.reverseBytes(Helper.doubleDigest(txData))); // // Calculate the normalized transaction ID // List<byte[]> bufferList = new ArrayList<>(txInputs.size()+txOutputs.size()); for(TransactionInput tx: txInputs){ bufferList.add(tx.getOutPoint().getBytes()); } for(TransactionOutput tx: txOutputs){ bufferList.add(tx.getBytes()); } normID = new Sha256Hash(Helper.reverseBytes(Helper.doubleDigest(bufferList))); } /** * Creates a new transaction from the serialized data in the byte stream * * @param inBuffer Serialized buffer * @throws EOFException Byte stream is too short * @throws VerificationException Verification error */ public Transaction(SerializedBuffer inBuffer) throws EOFException, VerificationException { // // Mark our current position within the input stream // int segmentStart = inBuffer.getSegmentStart(); inBuffer.setSegmentStart(); // // Get the transaction version // txVersion = inBuffer.getInt(); // // Get the transaction inputs // int inCount = inBuffer.getVarInt(); if (inCount < 0) throw new VerificationException("Transaction input count is negative"); txInputs = new ArrayList<>(Math.max(inCount, 1)); for (int i=0; i<inCount; i++) txInputs.add(new TransactionInput(this, i, inBuffer)); // // A coinbase transaction has a single unconnected input with a transaction hash of zero // and an output index of -1 // if (txInputs.size() == 1) { OutPoint outPoint = txInputs.get(0).getOutPoint(); coinBase = (outPoint.getHash().equals(Sha256Hash.ZERO_HASH) && outPoint.getIndex() == -1); } else { coinBase = false; } // // Get the transaction outputs // int outCount = inBuffer.getVarInt(); if (outCount < 0) throw new EOFException("Transaction output count is negative"); txOutputs = new ArrayList<>(Math.max(outCount, 1)); for (int i=0; i<outCount; i++) txOutputs.add(new TransactionOutput(i, inBuffer)); // // Get the transaction lock time // txLockTime = inBuffer.getUnsignedInt(); // // Save a copy of the serialized transaction // txData = inBuffer.getSegmentBytes(); // // Calculate the transaction hash using the serialized data // txHash = new Sha256Hash(Helper.reverseBytes(Helper.doubleDigest(txData))); // // Calculate the normalized transaction ID // List<byte[]> bufferList = new ArrayList<>(txInputs.size()+txOutputs.size()); for(TransactionInput tx: txInputs){ bufferList.add(tx.getOutPoint().getBytes()); } for(TransactionOutput tx: txOutputs){ bufferList.add(tx.getBytes()); } normID = new Sha256Hash(Helper.reverseBytes(Helper.doubleDigest(bufferList))); // // Restore the previous segment (if any) // inBuffer.setSegmentStart(segmentStart); } /** * Serialize the transaction * * @param outBuffer Output buffer * @return Output buffer */ @Override public final SerializedBuffer getBytes(SerializedBuffer outBuffer) { if (txData != null) { outBuffer.putBytes(txData); } else { outBuffer.putInt(txVersion) .putVarInt(txInputs.size()) .putBytes(txInputs) .putVarInt(txOutputs.size()) .putBytes(txOutputs) .putUnsignedInt(txLockTime); } return outBuffer; } /** * Returns the original serialized transaction data * * @return Serialized transaction data */ @Override public byte[] getBytes() { return txData; } /** * Returns the transaction version * * @return Transaction version */ public long getVersion() { return txVersion; } /** * Returns the transaction lock time * * @return Transaction lock time or zero */ public long getLockTime() { return txLockTime; } /** * Returns the transaction hash * * @return Transaction hash */ public Sha256Hash getHash() { return txHash; } /** * Returns the transaction hash as a printable string * * @return Transaction hash */ public String getHashAsString() { return txHash.toString(); } /** * Returns the normalized transaction ID * * @return Normalized transaction ID */ public Sha256Hash getNormalizedID() { return normID; } /** * Returns the list of transaction inputs * * @return List of transaction inputs */ public List<TransactionInput> getInputs() { return txInputs; } /** * Returns the list of transaction outputs * * @return List of transaction outputs */ public List<TransactionOutput> getOutputs() { return txOutputs; } /** * Checks if this is the coinbase transaction * * @return TRUE if this is the coinbase transaction */ public boolean isCoinBase() { return coinBase; } /** * Returns the hash code for this transaction. This is based on the transaction hash but is * not the same value. * * @return Hash code */ @Override public int hashCode() { return getHash().hashCode(); } /** * Compare this transaction to another transaction to determine if they are equal. * * @param obj The transaction to compare * @return TRUE if they are equal */ @Override public boolean equals(Object obj) { boolean areEqual = false; if (obj != null && (obj instanceof Transaction)) areEqual = getHash().equals(((Transaction)obj).getHash()); return areEqual; } /** * Returns a string representation of this transaction * * @return Formatted string */ @Override public String toString() { return String.format("Transaction: %s\n %d inputs, %d outputs, %s", getHashAsString(), txInputs.size(), txOutputs.size(), (coinBase ? "Coinbase" : "Not coinbase")); } /** * <p>Verify the transaction structure as follows</p> * <ul> * <li>A transaction must have at least one input and one output</li> * <li>A transaction output may not specify a negative number of coins</li> * <li>The sum of all of the output amounts must not exceed 21,000,000 BTC</li> * <li>A non-coinbase transaction may not contain any unconnected inputs</li> * <li>A connected output may not be used by more than one input</li> * <li>The input script must contain only push-data operations</li> * </ul> * * @param canonical TRUE to enforce canonical transactions * @throws VerificationException Script verification failed */ public void verify(boolean canonical) throws VerificationException { try { // Must have at least one input and one output if (txInputs.isEmpty() || txOutputs.isEmpty()) throw new VerificationException("Transaction does not have at least 1 input and 1 output", RejectMessage.REJECT_INVALID, txHash); // No output value may be negative // Sum of all output values must not exceed MAX_MONEY BigInteger outTotal = BigInteger.ZERO; for (TransactionOutput txOut : txOutputs) { BigInteger outValue = txOut.getValue(); if (outValue.signum() < 0) throw new VerificationException("Transaction output value is negative", RejectMessage.REJECT_INVALID, txHash); outTotal = outTotal.add(outValue); if (outTotal.compareTo(NetParams.MAX_MONEY) > 0) throw new VerificationException("Total transaction output amount exceeds maximum", RejectMessage.REJECT_INVALID, txHash); // byte[] scriptBytes = txOut.getScriptBytes(); } if (!coinBase) { // All inputs must have connected outputs // No outpoint may be used more than once // Input scripts must consist of only push-data operations List<OutPoint> outPoints = new ArrayList<>(txInputs.size()); for (TransactionInput txIn : txInputs) { OutPoint outPoint = txIn.getOutPoint(); if (outPoint.getHash().equals(Sha256Hash.ZERO_HASH) || outPoint.getIndex() < 0) throw new VerificationException("Non-coinbase transaction contains unconnected inputs", RejectMessage.REJECT_INVALID, txHash); if (outPoints.contains(outPoint)) throw new VerificationException("Connected output used in multiple inputs", RejectMessage.REJECT_INVALID, txHash); outPoints.add(outPoint); if (canonical) { if (!Script.checkInputScript(txIn.getScriptBytes())) throw new VerificationException("Input script must contain only canonical push-data operations", RejectMessage.REJECT_NONSTANDARD, txHash); } } } } catch (EOFException exc) { throw new VerificationException("End-of-data while processing script", RejectMessage.REJECT_MALFORMED, txHash); } } /** * Serializes the transaction for use in a signature * * @param index Current transaction index * @param sigHashType Signature hash type * @param subScriptBytes Replacement script for the current input * @param outBuffer Output buffer * @throws ScriptException Transaction index out-of-range */ public final void serializeForSignature(int index, int sigHashType, byte[] subScriptBytes, SerializedBuffer outBuffer) throws ScriptException { int hashType; boolean anyoneCanPay; // // The transaction input must be within range // if (index < 0 || index >= txInputs.size()) throw new ScriptException("Transaction input index is not valid"); // // Check for a valid hash type // // Note that SIGHASH_ANYONE_CAN_PAY is or'ed with one of the other hash types. So we need // to remove it when checking for a valid signature. // // SIGHASH_ALL: This is the default. It indicates that everything about the transaction is signed // except for the input scripts. Signing the input scripts as well would obviously make // it impossible to construct a transaction. // SIGHASH_NONE: The outputs are not signed and can be anything. This mode allows others to update // the transaction by changing their inputs sequence numbers. This means that all // input sequence numbers are set to 0 except for the current input. // SIGHASH_SINGLE: Outputs up to and including the current input index number are included. Outputs // before the current index have a -1 value and an empty script. All input sequence // numbers are set to 0 except for the current input. // // The SIGHASH_ANYONE_CAN_PAY modifier can be combined with the above three modes. When set, only that // input is signed and the other inputs can be anything. // // In all cases, the script for the current input is replaced with the script from the connected // output. All other input scripts are set to an empty script. // // The reference client accepts an invalid hash types and treats it as SIGHASH_ALL. So we need to // do the same. // anyoneCanPay = ((sigHashType&ScriptOpCodes.SIGHASH_ANYONE_CAN_PAY) != 0); hashType = sigHashType&(255-ScriptOpCodes.SIGHASH_ANYONE_CAN_PAY); if (hashType != ScriptOpCodes.SIGHASH_ALL && hashType != ScriptOpCodes.SIGHASH_NONE && hashType != ScriptOpCodes.SIGHASH_SINGLE) hashType = ScriptOpCodes.SIGHASH_ALL; // // Serialize the version // outBuffer.putInt(txVersion); // // Serialize the inputs // // For SIGHASH_ANYONE_CAN_PAY, only the current input is included in the signature. // Otherwise, all inputs are included. // List<TransactionInput> sigInputs; if (anyoneCanPay) { sigInputs = new ArrayList<>(1); sigInputs.add(txInputs.get(index)); } else { sigInputs = txInputs; } outBuffer.putVarInt(sigInputs.size()); byte[] emptyScriptBytes = new byte[0]; for (TransactionInput txInput : sigInputs) txInput.serializeForSignature(index, hashType, (txInput.getIndex()==index?subScriptBytes:emptyScriptBytes), outBuffer); // // Serialize the outputs // if (hashType == ScriptOpCodes.SIGHASH_NONE) { // // There are no outputs for SIGHASH_NONE // outBuffer.putVarInt(0); } else if (hashType == ScriptOpCodes.SIGHASH_SINGLE) { // // The output list is resized to the input index+1 // if (txOutputs.size() <= index) throw new ScriptException("Input index out-of-range for SIGHASH_SINGLE"); outBuffer.putVarInt(index+1); for (TransactionOutput txOutput : txOutputs) { if (txOutput.getIndex() > index) break; txOutput.serializeForSignature(index, hashType, outBuffer); } } else { // // All outputs are serialized for SIGHASH_ALL // outBuffer.putVarInt(txOutputs.size()); for (TransactionOutput txOutput : txOutputs) txOutput.serializeForSignature(index, hashType, outBuffer); } // // Serialize the lock time // outBuffer.putUnsignedInt(txLockTime); } }