bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.65k stars 2.09k forks source link

Imposible to spend CLTV locked input #1467

Closed volbil closed 5 years ago

volbil commented 5 years ago

Hello, for few days I'm trying to figure out how to spend time locked freez input which looks like this:

<expiry time> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <pubKeyHash> EQUALVERIFY CHECKSIG

Pretty much what is described in "Freezing Funds" chapter of original BIP65 proposal.

Here is simplified version of code which I'm using:

const private_key = "6L6fKZ6vdxw5K57QUFhoa4Szwboo4LVUWTJMLDkFJz7fCLr9WWQR"; // Private key
const change_address = "AKCfdVGaszaqkt4Etq6RPDtAo89oYRjHeu" // Change address
const timestamp = parseInt(Date.now() / 1000); // Current timestamp
const height = 300000; // Current network height
const fee = 10000 // Transaction fee

// UTXO which we trying to spend
const utxo = {
    script: "03400d03b17576a9149be07f1e973faf55dd2d03a495114f42a9540e4488ac",
    tx_hash: "7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c",
    value: 100000000,
    pos: 0
};

// Output of transaction
const output = {
    address: "AU2PwLKK3rF3HTWoerwFaC4WtocFsNzb6x",
    amount: 50000000
};

const txb = new bitcoin.TransactionBuilder(network)
txb.setVersion(1);
txb.setTimestamp(height);

// Check if our input is OP_CHECKLOCKTIMEVERIFY locked
var decodedScript = bitcoin.script.decompile(new Buffer(utxo[k].script, 'hex'));
if (decodedScript.includes(bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY)) {
    if (Buffer.isBuffer(decodedScript[0])) {
        // Make sure we comparing apples to apples
        if (decodedScript[0].readUIntLE(0, decodedScript[0].length) > 50000000) {
            // If input locktime is timestamp - we should set nTimeLock of transaction to current time
            txb.setLockTime(timestamp);
        }
    }

    // If it's OP_CHECKLOCKTIMEVERIFY locked input we should set nSequence to 0xfffffffe
    txb.addInput(utxo[k].tx_hash, utxo[k].tx_pos, 0xfffffffe);
} else {
    // Otherwise just add input
    txb.addInput(utxo[k].tx_hash, utxo[k].tx_pos);
}

txb.addOutput(output.address, output.amount); // Add output
txb.addOutput(change_address, utxo.value - amount - fee); // Add change output

// Sign input
txb.sign({
    vin: 0,
    keyPair: bitcoin.ECPair.fromWIF(address_key, network)
})

// Output raw transaction
console.log(txb.build().toHex());

All sensitive data in this example (like private key) is just placeholders :)

And here is error which I'm getting after to broadcasting final transaction:

16: mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)

Seams like problems is somehow related to the way in which I'm signing this input. I've read some related issues in this repo and tried to add utxo amount to txb.sign function (this didn't help) and also tried to add utxo script but following error message pop up:

Error: nonstandard not supported (400d03 OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 9be07f1e973faf55dd2d03a495114f42a9540e44 OP_EQUALVERIFY OP_CHECKSIG)

Seams like bitcoinjs-lib don't support such type of scripts. Not sure if they needed when I'm trying to sign time locked p2pkh but maybe this could be the problem.

P.s. little bit more info which may be useful: I'm running private testnet on vanila Bitcoin Core 0.15 with CLTV freez transaction support added.

junderw commented 5 years ago

Please look over the CLTV examples.

https://github.com/bitcoinjs/bitcoinjs-lib/blob/07a2769949c5074a64177f934b0a4a708d6fd277/test/integration/cltv.js

Try fixing it. And get back with any questions.

Thanks.

volbil commented 5 years ago

Hello @junderw. I already tried to use examples from cltv.js file, especially this part but it didn't help in any way. I also tried many different proposed solutions from closed issues in this repository but none of them works or fits in this problem description. Actually creating issue here is kind of last resort because I'm out of ideas :)

volbil commented 5 years ago

Most of examples in cltv.js related to creating time lock transaction, but not spending time lock inputs, especialy like freeze lock script for p2pkh. So I'm wondering if it's even possible to sign such input with bitcoinjs-lib:

<expiry time> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <pubKeyHash> EQUALVERIFY CHECKSIG
volbil commented 5 years ago

Sorry if I'm missing something way to obvious 😅

junderw commented 5 years ago

of course it is possible.

and there are examples of signing and broadcasting, that is literally what the tests are for.

I will show you an example with your specific script once I get some free time. But it will be very similar to the examples linked.

volbil commented 5 years ago

@jundrew this would be amazing! I probably missed some key point from example and additional one would be good for cross check.

junderw commented 5 years ago

This worked with only 2 changes (see comments for where I changed)

  function cltvBasicTemplate (ecPair, lockTime) { // NEW FUNCTION FOR CHANGE #1 
    return bitcoin.script.fromASM(`
      ${bitcoin.script.number.encode(lockTime).toString('hex')}
      OP_CHECKLOCKTIMEVERIFY
      OP_DROP
      OP_DUP
      OP_HASH160
      ${bitcoin.crypto.hash160(ecPair.publicKey).toString('hex')}
      OP_EQUALVERIFY
      OP_CHECKSIG
    `.trim().replace(/\s+/g, ' '))
  }

  // expiry past, {Alice's signature} OP_TRUE
  it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry (in the past)', async () => {
    // 3 hours ago
    const lockTime = bip65.encode({ utc: utcNow() - (3600 * 3) })
    const redeemScript = cltvBasicTemplate(alice, lockTime) // CHANGE #1 
    const { address } = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network: regtest }, network: regtest })

    // fund the P2SH(CLTV) address
    const unspent = await regtestUtils.faucet(address, 1e5)
    const txb = new bitcoin.TransactionBuilder(regtest)
    txb.setLockTime(lockTime)
    // Note: nSequence MUST be <= 0xfffffffe otherwise LockTime is ignored, and is immediately spendable.
    txb.addInput(unspent.txId, unspent.vout, 0xfffffffe)
    txb.addOutput(regtestUtils.RANDOM_ADDRESS, 7e4)

    // {Alice's signature} OP_TRUE
    const tx = txb.buildIncomplete()
    const signatureHash = tx.hashForSignature(0, redeemScript, hashType)
    const redeemScriptSig = bitcoin.payments.p2sh({
      redeem: {
        input: bitcoin.script.compile([
          bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
          alice.publicKey, // CHANGE #2 
        ]),
        output: redeemScript
      }
    }).input
    tx.setInputScript(0, redeemScriptSig)

    await regtestUtils.broadcast(tx.toHex())

    await regtestUtils.verify({
      txId: tx.getId(),
      address: regtestUtils.RANDOM_ADDRESS,
      vout: 0,
      value: 7e4
    })
  })
volbil commented 5 years ago

Thanks @junderw! Will check this in a moment :)

volbil commented 5 years ago

Hello again @junderw, thanks for your help, I successfully managed to sign and spent time locked utxo :D

Here is final version of signing code for everyone who is interested:

// Previous part of code from my initial example is unchanged
// Input signing part
const redeemScript = new Buffer(utxo.script, 'hex');
const tx = txb.buildIncomplete()
const signatureHash = tx.hashForSignature(0, redeemScript, hashType)
const redeemScriptSig = bitcoin.payments.p2sh({
    redeem: {
        input: bitcoin.script.compile([
            bitcoin.script.signature.encode(alice.sign(signatureHash), hashType),
            keys.publicKey,
        ]),
        output: redeemScript,
        network: network
    }, network: network
})

tx.setInputScript(0, redeemScriptSig.redeem.input)

// Output raw transaction
console.log(txb.build().toHex());