BitGo / bitgo-utxo-lib

UTXO coins functions implemented in pure JavaScript
83 stars 142 forks source link

Building/Signing Zcash Testnet Transactions #77

Closed zquestz closed 4 years ago

zquestz commented 4 years ago

I am attempting to build 2 functions with this library, one that constructs an unsigned transaction, and another that takes that unsigned transaction and signs it then returns the needed hex.

After some playing around, I got it to produce a transaction the library thinks is valid, but doesn't broadcast on the network. =\

NOTE: I am doing this for TESTNET!

I am probably just missing something simple... so figured I could share some code. =)

/**
 * Returns the network object from the zcash library
 * @param input.network Network string passed into coin-ops
 * @returns the zcash network
 */
function zecNetwork({ network }: ZecNetworkInput): any {
  switch (network) {
    case "zcash-mainnet":
      return bitgo.networks.zcash;
    case "zcash-testnet":
      return bitgo.networks.zcashTest;
    default:
      throw new ValidationError(`invalid network`);
  }
}

/**
 * Constructs a UTXO transaction.
 * @param input.inputs The intended inputs for the transaction
 * @param input.outputs The intended outputs for the transaction
 * @param input.options The options to use when constructing
 * @returns output.unsigned_tx The unsigned transaction as hex
 */
export async function constructUTXOTransaction({
  inputs,
  outputs,
  options,
  network
}: ConstructUTXOTransactionInput): Promise<ConstructUTXOTransactionOutput> {
  const txb = new bitgo.TransactionBuilder(zecNetwork({ network }));

  txb.setVersion(4);
  txb.setVersionGroupId(0x892f2085);
  txb.setExpiryHeight(0);

  inputs.forEach((value, index, array) => {
    txb.addInput(value.identifier.hsh, value.identifier.index);
  });

  outputs.forEach((value, index, array) => {
    txb.addOutput(
      bitgo.address.toOutputScript(value.address, zecNetwork({ network })),
      value.quantity
    );
  });

  return { unsigned_tx: txb.buildIncomplete().toHex() };
}

/**
 * Signs a UTXO transaction.
 * @param input.unsigned_tx The unsigned transaction in hex format.
 * @param input.private_blobs A JSON-encoded list of private blobs to use
 * @param input.key_id The KMS key to decrypt the encoded list of private_blobs
 * @param input.network The network name to derive the blockchain settings for
 * @param input.utxo_indices The private key to use for each utxo
 * @param input.inputs The inputs to the contstructed transaction
 * @returns output.signed_tx The signed transaction
 */
export async function signUTXOTransaction({
  unsigned_tx,
  private_blobs,
  inputs,
  key_id,
  network,
  utxo_indices
}: SignUTXOTransactionInput): Promise<SignUTXOTransactionOutput> {
  const txb = bitgo.TransactionBuilder.fromTransaction(
    bitgo.Transaction.fromHex(unsigned_tx, zecNetwork({ network })),
    zecNetwork({ network })
  );

  const privateKeypairs = [];

  if (utxo_indices.length !== inputs.length) {
    throw new ValidationError(`invalid utxo_indices`);
  }

  for (const val of utxo_indices) {
    const privateBlob = await kmsDecrypt(private_blobs[val], key_id);
    const ecPair = bitgo.ECPair.fromPrivateKeyBuffer(
      Buffer.from(privateBlob, "hex"),
      zecNetwork({ network })
    );

    privateKeypairs.push(ecPair);
  }

  inputs.forEach((value, index, array) => {
    txb.sign(
      index,
      privateKeypairs[index],
      "",
      bitgo.Transaction.SIGHASH_SINGLE,
      value.amount.amount
    );
  });

  return {
    signed_tx: txb.build().toHex()
  };
}

When I submit the final tx to the network I get

{
    "result": null,
    "error": {
        "code": -26,
        "message": "16: mandatory-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)"
    },
    "id": 1
}

Here is the final signed tx hex:

0400008085202f89018379bd136089765ee9b47504540619b0cf3a8e481659551a06fc875db14ff9e2000000006b483045022100c57290968719bbedfc6e41c5b62744c125e3a9c39e626ee42291771c481231760220453b0ac696a268b72b4952e2f485ea114823557f348c2f5ae76d7da3cce716a5032102594fdae20bf32bce9d3d072813d7e5475d56a13134baa63650916d07b7985dc5ffffffff0100e1f505000000001976a914e44661aa9f4ffbe9066f90283078adf3a896b6d388ac00000000000000000000000000000000000000

The decoded transaction in json:

{
   "result":{
      "txid":"52302920753a6eef7212797ee08251568d832f828b4dc505e8770a95329f51df",
      "overwintered":true,
      "version":4,
      "versiongroupid":"892f2085",
      "locktime":0,
      "expiryheight":0,
      "vin":[
         {
            "txid":"e2f94fb15d87fc061a555916488e3acfb01906540475b4e95e76896013bd7983",
            "vout":0,
            "scriptSig":{
               "asm":"3045022100a01fcec97fb383bc5fee536893479be0bb34744abdfa44efcdb7c9b9b39d858602200e8c0b75acabbed1e56d5e465f1850731d813421936bf8e7fe5be6bbc6c1c0d3[SINGLE] 02594fdae20bf32bce9d3d072813d7e5475d56a13134baa63650916d07b7985dc5",
               "hex":"483045022100a01fcec97fb383bc5fee536893479be0bb34744abdfa44efcdb7c9b9b39d858602200e8c0b75acabbed1e56d5e465f1850731d813421936bf8e7fe5be6bbc6c1c0d3032102594fdae20bf32bce9d3d072813d7e5475d56a13134baa63650916d07b7985dc5"
            },
            "sequence":4294967295
         }
      ],
      "vout":[
         {
            "value":1.00000000,
            "valueZat":100000000,
            "valueSat":100000000,
            "n":0,
            "scriptPubKey":{
               "asm":"OP_DUP OP_HASH160 e44661aa9f4ffbe9066f90283078adf3a896b6d3 OP_EQUALVERIFY OP_CHECKSIG",
               "hex":"76a914e44661aa9f4ffbe9066f90283078adf3a896b6d388ac",
               "reqSigs":1,
               "type":"pubkeyhash",
               "addresses":[
                  "tmWXMi6sHPxoKP5xqLK9Lqhxfg5hJqwsDvS"
               ]
            }
         }
      ],
      "vjoinsplit":[

      ],
      "valueBalance":0.00000000,
      "valueBalanceZat":0,
      "vShieldedSpend":[

      ],
      "vShieldedOutput":[

      ]
   },
   "error":null,
   "id":"curltest"
}
zquestz commented 4 years ago

I also noticed the consensus ID is wrong...

    {
        /*.nBranchId =*/ 0xf5b9230b,
        /*.strName =*/ "Heartwood",
        /*.strInfo =*/ "See https://z.cash/upgrade/heartwood/ for details.",
    },

For v4 txs on testnet it should now be 0xf5b9230b.

For now I have patched my network lookup method.

/**
 * Returns the network object from the zcash library
 * @param input.network Network string passed into coin-ops
 * @returns the zcash network
 */
function zecNetwork({ network }: ZecNetworkInput): any {
  var net = null;

  switch (network) {
    case "zcash-mainnet":
      net = bitgo.networks.zcash;
      break;
    case "zcash-testnet":
      net = bitgo.networks.zcashTest;
      net.consensusBranchId['4'] = 0xf5b9230b
      break;
    default:
      throw new ValidationError(`invalid network`);
  }

  return net
}

However the resulting tx still doesn't broadcast. =\

0400008085202f89018379bd136089765ee9b47504540619b0cf3a8e481659551a06fc875db14ff9e2000000006b483045022100a01fcec97fb383bc5fee536893479be0bb34744abdfa44efcdb7c9b9b39d858602200e8c0b75acabbed1e56d5e465f1850731d813421936bf8e7fe5be6bbc6c1c0d3032102594fdae20bf32bce9d3d072813d7e5475d56a13134baa63650916d07b7985dc5ffffffff0100e1f505000000001976a914e44661aa9f4ffbe9066f90283078adf3a896b6d388ac00000000000000000000000000000000000000
zquestz commented 4 years ago

Got it working. Going to close this ticket, but you should really fix the other zcash issue in issue #61.