/**
  * When the servers signature for the refund transaction is received, call this to verify it and
  * sign the complete refund ourselves.
  *
  * <p>If this does not throw an exception, we are secure against the loss of funds and can safely
  * provide the server with the multi-sig contract to lock in the agreement. In this case, both the
  * multisig contract and the refund transaction are automatically committed to wallet so that it
  * can handle broadcasting the refund transaction at the appropriate time if necessary.
  */
 public synchronized void provideRefundSignature(byte[] theirSignature)
     throws VerificationException {
   checkNotNull(theirSignature);
   checkState(state == State.WAITING_FOR_SIGNED_REFUND);
   TransactionSignature theirSig = TransactionSignature.decodeFromBitcoin(theirSignature, true);
   if (theirSig.sigHashMode() != Transaction.SigHash.NONE || !theirSig.anyoneCanPay())
     throw new VerificationException("Refund signature was not SIGHASH_NONE|SIGHASH_ANYONECANPAY");
   // Sign the refund transaction ourselves.
   final TransactionOutput multisigContractOutput = multisigContract.getOutput(0);
   try {
     multisigScript = multisigContractOutput.getScriptPubKey();
   } catch (ScriptException e) {
     throw new RuntimeException(e); // Cannot happen: we built this ourselves.
   }
   TransactionSignature ourSignature =
       refundTx.calculateSignature(0, myKey, multisigScript, Transaction.SigHash.ALL, false);
   // Insert the signatures.
   Script scriptSig = ScriptBuilder.createMultiSigInputScript(ourSignature, theirSig);
   log.info("Refund scriptSig: {}", scriptSig);
   log.info("Multi-sig contract scriptPubKey: {}", multisigScript);
   TransactionInput refundInput = refundTx.getInput(0);
   refundInput.setScriptSig(scriptSig);
   refundInput.verify(multisigContractOutput);
   state = State.SAVE_STATE_IN_WALLET;
 }
Exemple #2
0
  @Test
  public void testCreateMultiSigInputScript() throws AddressFormatException {
    // Setup transaction and signatures
    ECKey key1 =
        new DumpedPrivateKey(params, "cVLwRLTvz3BxDAWkvS3yzT9pUcTCup7kQnfT2smRjvmmm1wAP6QT")
            .getKey();
    ECKey key2 =
        new DumpedPrivateKey(params, "cTine92s8GLpVqvebi8rYce3FrUYq78ZGQffBYCS1HmDPJdSTxUo")
            .getKey();
    ECKey key3 =
        new DumpedPrivateKey(params, "cVHwXSPRZmL9adctwBwmn4oTZdZMbaCsR5XF6VznqMgcvt1FDDxg")
            .getKey();
    Script multisigScript =
        ScriptBuilder.createMultiSigOutputScript(2, Arrays.asList(key1, key2, key3));
    byte[] bytes =
        Hex.decode(
            "01000000013df681ff83b43b6585fa32dd0e12b0b502e6481e04ee52ff0fdaf55a16a4ef61000000006b483045022100a84acca7906c13c5895a1314c165d33621cdcf8696145080895cbf301119b7cf0220730ff511106aa0e0a8570ff00ee57d7a6f24e30f592a10cae1deffac9e13b990012102b8d567bcd6328fd48a429f9cf4b315b859a58fd28c5088ef3cb1d98125fc4e8dffffffff02364f1c00000000001976a91439a02793b418de8ec748dd75382656453dc99bcb88ac40420f000000000017a9145780b80be32e117f675d6e0ada13ba799bf248e98700000000");
    Transaction transaction = new Transaction(params, bytes);
    TransactionOutput output = transaction.getOutput(1);
    Transaction spendTx = new Transaction(params);
    Address address = new Address(params, "n3CFiCmBXVt5d3HXKQ15EFZyhPz4yj5F3H");
    Script outputScript = ScriptBuilder.createOutputScript(address);
    spendTx.addOutput(output.getValue(), outputScript);
    spendTx.addInput(output);
    Sha256Hash sighash = spendTx.hashForSignature(0, multisigScript, SigHash.ALL, false);
    ECKey.ECDSASignature party1Signature = key1.sign(sighash);
    ECKey.ECDSASignature party2Signature = key2.sign(sighash);
    TransactionSignature party1TransactionSignature =
        new TransactionSignature(party1Signature, SigHash.ALL, false);
    TransactionSignature party2TransactionSignature =
        new TransactionSignature(party2Signature, SigHash.ALL, false);

    // Create p2sh multisig input script
    Script inputScript =
        ScriptBuilder.createP2SHMultiSigInputScript(
            ImmutableList.of(party1TransactionSignature, party2TransactionSignature),
            multisigScript.getProgram());

    // Assert that the input script contains 4 chunks
    assertTrue(inputScript.getChunks().size() == 4);

    // Assert that the input script created contains the original multisig
    // script as the last chunk
    ScriptChunk scriptChunk = inputScript.getChunks().get(inputScript.getChunks().size() - 1);
    Assert.assertArrayEquals(scriptChunk.data, multisigScript.getProgram());

    // Create regular multisig input script
    inputScript =
        ScriptBuilder.createMultiSigInputScript(
            ImmutableList.of(party1TransactionSignature, party2TransactionSignature));

    // Assert that the input script only contains 3 chunks
    assertTrue(inputScript.getChunks().size() == 3);

    // Assert that the input script created does not end with the original
    // multisig script
    scriptChunk = inputScript.getChunks().get(inputScript.getChunks().size() - 1);
    Assert.assertThat(scriptChunk.data, IsNot.not(IsEqual.equalTo(multisigScript.getProgram())));
  }
 /**
  * Creates the initial multisig contract and incomplete refund transaction which can be requested
  * at the appropriate time using {@link PaymentChannelClientState#getIncompleteRefundTransaction}
  * and {@link PaymentChannelClientState#getMultisigContract()}. The way the contract is crafted
  * can be adjusted by overriding {@link
  * PaymentChannelClientState#editContractSendRequest(com.google.bitcoin.core.Wallet.SendRequest)}.
  * By default unconfirmed coins are allowed to be used, as for micropayments the risk should be
  * relatively low.
  *
  * @throws ValueOutOfRangeException if the value being used is too small to be accepted by the
  *     network
  * @throws InsufficientMoneyException if the wallet doesn't contain enough balance to initiate
  */
 public synchronized void initiate() throws ValueOutOfRangeException, InsufficientMoneyException {
   final NetworkParameters params = wallet.getParams();
   Transaction template = new Transaction(params);
   // We always place the client key before the server key because, if either side wants some
   // privacy, they can
   // use a fresh key for the the multisig contract and nowhere else
   List<ECKey> keys = Lists.newArrayList(myKey, serverMultisigKey);
   // There is also probably a change output, but we don't bother shuffling them as it's obvious
   // from the
   // format which one is the change. If we start obfuscating the change output better in future
   // this may
   // be worth revisiting.
   TransactionOutput multisigOutput =
       template.addOutput(totalValue, ScriptBuilder.createMultiSigOutputScript(2, keys));
   if (multisigOutput.getMinNonDustValue().compareTo(totalValue) > 0)
     throw new ValueOutOfRangeException("totalValue too small to use");
   Wallet.SendRequest req = Wallet.SendRequest.forTx(template);
   req.coinSelector = AllowUnconfirmedCoinSelector.get();
   editContractSendRequest(req);
   req.shuffleOutputs = false; // TODO: Fix things so shuffling is usable.
   wallet.completeTx(req);
   Coin multisigFee = req.tx.getFee();
   multisigContract = req.tx;
   // Build a refund transaction that protects us in the case of a bad server that's just trying to
   // cause havoc
   // by locking up peoples money (perhaps as a precursor to a ransom attempt). We time lock it so
   // the server
   // has an assurance that we cannot take back our money by claiming a refund before the channel
   // closes - this
   // relies on the fact that since Bitcoin 0.8 time locked transactions are non-final. This will
   // need to change
   // in future as it breaks the intended design of timelocking/tx replacement, but for now it
   // simplifies this
   // specific protocol somewhat.
   refundTx = new Transaction(params);
   refundTx
       .addInput(multisigOutput)
       .setSequenceNumber(0); // Allow replacement when it's eventually reactivated.
   refundTx.setLockTime(expiryTime);
   if (totalValue.compareTo(Coin.CENT) < 0) {
     // Must pay min fee.
     final Coin valueAfterFee = totalValue.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
     if (Transaction.MIN_NONDUST_OUTPUT.compareTo(valueAfterFee) > 0)
       throw new ValueOutOfRangeException("totalValue too small to use");
     refundTx.addOutput(valueAfterFee, myKey.toAddress(params));
     refundFees = multisigFee.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
   } else {
     refundTx.addOutput(totalValue, myKey.toAddress(params));
     refundFees = multisigFee;
   }
   refundTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
   log.info(
       "initiated channel with multi-sig contract {}, refund {}",
       multisigContract.getHashAsString(),
       refundTx.getHashAsString());
   state = State.INITIATED;
   // Client should now call getIncompleteRefundTransaction() and send it to the server.
 }
Exemple #4
0
  public Address getAddressForOutput(TransactionOutput out) {
    try {
      Script script = out.getScriptPubKey();
      if (script.isSentToRawPubKey()) {
        byte[] key = out.getScriptPubKey().getPubKey();
        byte[] address_bytes = com.google.bitcoin.core.Utils.sha256hash160(key);
        Address a = new Address(params, address_bytes);
        return a;
      } else {
        Address a = script.getToAddress(params);
        return a;
      }
    } catch (ScriptException e) {

      // System.out.println(out.getParentTransaction().getHash() + " - " + out);
      // e.printStackTrace();
      // jelly.getEventLog().log("Unable process tx output: " +
      // out.getParentTransaction().getHash());
    }
    return null;
  }
  @Override
  /** Used during reorgs to connect a block previously on a fork */
  protected synchronized TransactionOutputChanges connectTransactions(StoredBlock newBlock)
      throws VerificationException, BlockStoreException, PrunedException {
    checkState(lock.isHeldByCurrentThread());
    if (!params.passesCheckpoint(newBlock.getHeight(), newBlock.getHeader().getHash()))
      throw new VerificationException("Block failed checkpoint lockin at " + newBlock.getHeight());

    blockStore.beginDatabaseBatchWrite();
    StoredUndoableBlock block = blockStore.getUndoBlock(newBlock.getHeader().getHash());
    if (block == null) {
      // We're trying to re-org too deep and the data needed has been deleted.
      blockStore.abortDatabaseBatchWrite();
      throw new PrunedException(newBlock.getHeader().getHash());
    }
    TransactionOutputChanges txOutChanges;
    try {
      List<Transaction> transactions = block.getTransactions();
      if (transactions != null) {
        LinkedList<StoredTransactionOutput> txOutsSpent = new LinkedList<StoredTransactionOutput>();
        LinkedList<StoredTransactionOutput> txOutsCreated =
            new LinkedList<StoredTransactionOutput>();
        long sigOps = 0;
        final boolean enforcePayToScriptHash =
            newBlock.getHeader().getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME;
        if (!params.isCheckpoint(newBlock.getHeight())) {
          for (Transaction tx : transactions) {
            Sha256Hash hash = tx.getHash();
            if (blockStore.hasUnspentOutputs(hash, tx.getOutputs().size()))
              throw new VerificationException("Block failed BIP30 test!");
          }
        }
        Coin totalFees = Coin.ZERO;
        Coin coinbaseValue = null;

        if (scriptVerificationExecutor.isShutdown())
          scriptVerificationExecutor =
              Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        List<Future<VerificationException>> listScriptVerificationResults =
            new ArrayList<Future<VerificationException>>(transactions.size());
        for (final Transaction tx : transactions) {
          boolean isCoinBase = tx.isCoinBase();
          Coin valueIn = Coin.ZERO;
          Coin valueOut = Coin.ZERO;
          final List<Script> prevOutScripts = new LinkedList<Script>();
          if (!isCoinBase) {
            for (int index = 0; index < tx.getInputs().size(); index++) {
              final TransactionInput in = tx.getInputs().get(index);
              final StoredTransactionOutput prevOut =
                  blockStore.getTransactionOutput(
                      in.getOutpoint().getHash(), in.getOutpoint().getIndex());
              if (prevOut == null)
                throw new VerificationException(
                    "Attempted spend of a non-existent or already spent output!");
              if (newBlock.getHeight() - prevOut.getHeight() < params.getSpendableCoinbaseDepth())
                throw new VerificationException(
                    "Tried to spend coinbase at depth "
                        + (newBlock.getHeight() - prevOut.getHeight()));
              valueIn = valueIn.add(prevOut.getValue());
              if (enforcePayToScriptHash) {
                Script script = new Script(prevOut.getScriptBytes());
                if (script.isPayToScriptHash())
                  sigOps += Script.getP2SHSigOpCount(in.getScriptBytes());
                if (sigOps > Block.MAX_BLOCK_SIGOPS)
                  throw new VerificationException("Too many P2SH SigOps in block");
              }

              prevOutScripts.add(new Script(prevOut.getScriptBytes()));

              blockStore.removeUnspentTransactionOutput(prevOut);
              txOutsSpent.add(prevOut);
            }
          }
          Sha256Hash hash = tx.getHash();
          for (TransactionOutput out : tx.getOutputs()) {
            valueOut = valueOut.add(out.getValue());
            StoredTransactionOutput newOut =
                new StoredTransactionOutput(
                    hash,
                    out.getIndex(),
                    out.getValue(),
                    newBlock.getHeight(),
                    isCoinBase,
                    out.getScriptBytes());
            blockStore.addUnspentTransactionOutput(newOut);
            txOutsCreated.add(newOut);
          }
          // All values were already checked for being non-negative (as it is verified in
          // Transaction.verify())
          // but we check again here just for defence in depth. Transactions with zero output value
          // are OK.
          if (valueOut.signum() < 0 || valueOut.compareTo(NetworkParameters.MAX_MONEY) > 0)
            throw new VerificationException("Transaction output value out of range");
          if (isCoinBase) {
            coinbaseValue = valueOut;
          } else {
            if (valueIn.compareTo(valueOut) < 0
                || valueIn.compareTo(NetworkParameters.MAX_MONEY) > 0)
              throw new VerificationException("Transaction input value out of range");
            totalFees = totalFees.add(valueIn.subtract(valueOut));
          }

          if (!isCoinBase) {
            // Because correctlySpends modifies transactions, this must come after we are done with
            // tx
            FutureTask<VerificationException> future =
                new FutureTask<VerificationException>(
                    new Verifier(tx, prevOutScripts, enforcePayToScriptHash));
            scriptVerificationExecutor.execute(future);
            listScriptVerificationResults.add(future);
          }
        }
        if (totalFees.compareTo(NetworkParameters.MAX_MONEY) > 0
            || newBlock
                    .getHeader()
                    .getBlockInflation(newBlock.getHeight())
                    .add(totalFees)
                    .compareTo(coinbaseValue)
                < 0) throw new VerificationException("Transaction fees out of range");
        txOutChanges = new TransactionOutputChanges(txOutsCreated, txOutsSpent);
        for (Future<VerificationException> future : listScriptVerificationResults) {
          VerificationException e;
          try {
            e = future.get();
          } catch (InterruptedException thrownE) {
            throw new RuntimeException(thrownE); // Shouldn't happen
          } catch (ExecutionException thrownE) {
            log.error("Script.correctlySpends threw a non-normal exception: " + thrownE.getCause());
            throw new VerificationException(
                "Bug in Script.correctlySpends, likely script malformed in some new and interesting way.",
                thrownE);
          }
          if (e != null) throw e;
        }
      } else {
        txOutChanges = block.getTxOutChanges();
        if (!params.isCheckpoint(newBlock.getHeight()))
          for (StoredTransactionOutput out : txOutChanges.txOutsCreated) {
            Sha256Hash hash = out.getHash();
            if (blockStore.getTransactionOutput(hash, out.getIndex()) != null)
              throw new VerificationException("Block failed BIP30 test!");
          }
        for (StoredTransactionOutput out : txOutChanges.txOutsCreated)
          blockStore.addUnspentTransactionOutput(out);
        for (StoredTransactionOutput out : txOutChanges.txOutsSpent)
          blockStore.removeUnspentTransactionOutput(out);
      }
    } catch (VerificationException e) {
      scriptVerificationExecutor.shutdownNow();
      blockStore.abortDatabaseBatchWrite();
      throw e;
    } catch (BlockStoreException e) {
      scriptVerificationExecutor.shutdownNow();
      blockStore.abortDatabaseBatchWrite();
      throw e;
    }
    return txOutChanges;
  }
  @Override
  protected TransactionOutputChanges connectTransactions(int height, Block block)
      throws VerificationException, BlockStoreException {
    checkState(lock.isHeldByCurrentThread());
    if (block.transactions == null)
      throw new RuntimeException(
          "connectTransactions called with Block that didn't have transactions!");
    if (!params.passesCheckpoint(height, block.getHash()))
      throw new VerificationException("Block failed checkpoint lockin at " + height);

    blockStore.beginDatabaseBatchWrite();

    LinkedList<StoredTransactionOutput> txOutsSpent = new LinkedList<StoredTransactionOutput>();
    LinkedList<StoredTransactionOutput> txOutsCreated = new LinkedList<StoredTransactionOutput>();
    long sigOps = 0;
    final boolean enforcePayToScriptHash =
        block.getTimeSeconds() >= NetworkParameters.BIP16_ENFORCE_TIME;

    if (scriptVerificationExecutor.isShutdown())
      scriptVerificationExecutor =
          Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    List<Future<VerificationException>> listScriptVerificationResults =
        new ArrayList<Future<VerificationException>>(block.transactions.size());
    try {
      if (!params.isCheckpoint(height)) {
        // BIP30 violator blocks are ones that contain a duplicated transaction. They are all in the
        // checkpoints list and we therefore only check non-checkpoints for duplicated transactions
        // here. See the
        // BIP30 document for more details on this:
        // https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki
        for (Transaction tx : block.transactions) {
          Sha256Hash hash = tx.getHash();
          // If we already have unspent outputs for this hash, we saw the tx already. Either the
          // block is
          // being added twice (bug) or the block is a BIP30 violator.
          if (blockStore.hasUnspentOutputs(hash, tx.getOutputs().size()))
            throw new VerificationException("Block failed BIP30 test!");
          if (enforcePayToScriptHash) // We already check non-BIP16 sigops in
            // Block.verifyTransactions(true)
            sigOps += tx.getSigOpCount();
        }
      }
      Coin totalFees = Coin.ZERO;
      Coin coinbaseValue = null;
      for (final Transaction tx : block.transactions) {
        boolean isCoinBase = tx.isCoinBase();
        Coin valueIn = Coin.ZERO;
        Coin valueOut = Coin.ZERO;
        final List<Script> prevOutScripts = new LinkedList<Script>();
        if (!isCoinBase) {
          // For each input of the transaction remove the corresponding output from the set of
          // unspent
          // outputs.
          for (int index = 0; index < tx.getInputs().size(); index++) {
            TransactionInput in = tx.getInputs().get(index);
            StoredTransactionOutput prevOut =
                blockStore.getTransactionOutput(
                    in.getOutpoint().getHash(), in.getOutpoint().getIndex());
            if (prevOut == null)
              throw new VerificationException(
                  "Attempted to spend a non-existent or already spent output!");
            // Coinbases can't be spent until they mature, to avoid re-orgs destroying entire
            // transaction
            // chains. The assumption is there will ~never be re-orgs deeper than the spendable
            // coinbase
            // chain depth.
            if (height - prevOut.getHeight() < params.getSpendableCoinbaseDepth())
              throw new VerificationException(
                  "Tried to spend coinbase at depth " + (height - prevOut.getHeight()));
            // TODO: Check we're not spending the genesis transaction here. Satoshis code won't
            // allow it.
            valueIn = valueIn.add(prevOut.getValue());
            if (enforcePayToScriptHash) {
              if (new Script(prevOut.getScriptBytes()).isPayToScriptHash())
                sigOps += Script.getP2SHSigOpCount(in.getScriptBytes());
              if (sigOps > Block.MAX_BLOCK_SIGOPS)
                throw new VerificationException("Too many P2SH SigOps in block");
            }

            prevOutScripts.add(new Script(prevOut.getScriptBytes()));

            // in.getScriptSig().correctlySpends(tx, index, new Script(params,
            // prevOut.getScriptBytes(), 0, prevOut.getScriptBytes().length));

            blockStore.removeUnspentTransactionOutput(prevOut);
            txOutsSpent.add(prevOut);
          }
        }
        Sha256Hash hash = tx.getHash();
        for (TransactionOutput out : tx.getOutputs()) {
          valueOut = valueOut.add(out.getValue());
          // For each output, add it to the set of unspent outputs so it can be consumed in future.
          StoredTransactionOutput newOut =
              new StoredTransactionOutput(
                  hash, out.getIndex(), out.getValue(), height, isCoinBase, out.getScriptBytes());
          blockStore.addUnspentTransactionOutput(newOut);
          txOutsCreated.add(newOut);
        }
        // All values were already checked for being non-negative (as it is verified in
        // Transaction.verify())
        // but we check again here just for defence in depth. Transactions with zero output value
        // are OK.
        if (valueOut.signum() < 0 || valueOut.compareTo(NetworkParameters.MAX_MONEY) > 0)
          throw new VerificationException("Transaction output value out of range");
        if (isCoinBase) {
          coinbaseValue = valueOut;
        } else {
          if (valueIn.compareTo(valueOut) < 0 || valueIn.compareTo(NetworkParameters.MAX_MONEY) > 0)
            throw new VerificationException("Transaction input value out of range");
          totalFees = totalFees.add(valueIn.subtract(valueOut));
        }

        if (!isCoinBase && runScripts) {
          // Because correctlySpends modifies transactions, this must come after we are done with tx
          FutureTask<VerificationException> future =
              new FutureTask<VerificationException>(
                  new Verifier(tx, prevOutScripts, enforcePayToScriptHash));
          scriptVerificationExecutor.execute(future);
          listScriptVerificationResults.add(future);
        }
      }
      if (totalFees.compareTo(NetworkParameters.MAX_MONEY) > 0
          || block.getBlockInflation(height).add(totalFees).compareTo(coinbaseValue) < 0)
        throw new VerificationException("Transaction fees out of range");
      for (Future<VerificationException> future : listScriptVerificationResults) {
        VerificationException e;
        try {
          e = future.get();
        } catch (InterruptedException thrownE) {
          throw new RuntimeException(thrownE); // Shouldn't happen
        } catch (ExecutionException thrownE) {
          log.error("Script.correctlySpends threw a non-normal exception: " + thrownE.getCause());
          throw new VerificationException(
              "Bug in Script.correctlySpends, likely script malformed in some new and interesting way.",
              thrownE);
        }
        if (e != null) throw e;
      }
    } catch (VerificationException e) {
      scriptVerificationExecutor.shutdownNow();
      blockStore.abortDatabaseBatchWrite();
      throw e;
    } catch (BlockStoreException e) {
      scriptVerificationExecutor.shutdownNow();
      blockStore.abortDatabaseBatchWrite();
      throw e;
    }
    return new TransactionOutputChanges(txOutsCreated, txOutsSpent);
  }
  private static Protos.Transaction makeTxProto(WalletTransaction wtx) {
    Transaction tx = wtx.getTransaction();
    Protos.Transaction.Builder txBuilder = Protos.Transaction.newBuilder();

    txBuilder
        .setPool(Protos.Transaction.Pool.valueOf(wtx.getPool().getValue()))
        .setHash(hashToByteString(tx.getHash()))
        .setVersion((int) tx.getVersion());

    if (tx.getUpdateTime() != null) {
      txBuilder.setUpdatedAt(tx.getUpdateTime().getTime());
    }

    if (tx.getLockTime() > 0) {
      txBuilder.setLockTime((int) tx.getLockTime());
    }

    // Handle inputs.
    for (TransactionInput input : tx.getInputs()) {
      Protos.TransactionInput.Builder inputBuilder =
          Protos.TransactionInput.newBuilder()
              .setScriptBytes(ByteString.copyFrom(input.getScriptBytes()))
              .setTransactionOutPointHash(hashToByteString(input.getOutpoint().getHash()))
              .setTransactionOutPointIndex((int) input.getOutpoint().getIndex());
      if (input.hasSequence()) {
        inputBuilder.setSequence((int) input.getSequenceNumber());
      }
      txBuilder.addTransactionInput(inputBuilder);
    }

    // Handle outputs.
    for (TransactionOutput output : tx.getOutputs()) {
      Protos.TransactionOutput.Builder outputBuilder =
          Protos.TransactionOutput.newBuilder()
              .setScriptBytes(ByteString.copyFrom(output.getScriptBytes()))
              .setValue(output.getValue().longValue());
      final TransactionInput spentBy = output.getSpentBy();
      if (spentBy != null) {
        Sha256Hash spendingHash = spentBy.getParentTransaction().getHash();
        int spentByTransactionIndex = spentBy.getParentTransaction().getInputs().indexOf(spentBy);
        outputBuilder
            .setSpentByTransactionHash(hashToByteString(spendingHash))
            .setSpentByTransactionIndex(spentByTransactionIndex);
      }
      txBuilder.addTransactionOutput(outputBuilder);
    }

    // Handle which blocks tx was seen in.
    if (tx.getAppearsInHashes() != null) {
      for (Sha256Hash hash : tx.getAppearsInHashes()) {
        txBuilder.addBlockHash(hashToByteString(hash));
      }
    }

    if (tx.hasConfidence()) {
      TransactionConfidence confidence = tx.getConfidence();
      Protos.TransactionConfidence.Builder confidenceBuilder =
          Protos.TransactionConfidence.newBuilder();
      writeConfidence(txBuilder, confidence, confidenceBuilder);
    }

    return txBuilder.build();
  }