cmdruid / tapscript

A humble library for working with Tapscript and Bitcoin Transactions.
https://www.npmjs.com/package/@cmdcode/tapscript
Creative Commons Zero v1.0 Universal
188 stars 49 forks source link

I can't get my Azure function to work in Docker when importing @cmdcode/tapscript #44

Closed ChristianOConnor closed 1 month ago

ChristianOConnor commented 1 month ago

I created an Azure function locally with the following commands in powershell: func init --worker-runtime node --language typescript --docker func new --name generateBitcoinAddress --template "HTTP trigger" This creates a boilerplate Azure function that looks like this:

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";

export async function generateBitcoinAddress(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);

    const name = request.query.get('name') || await request.text() || 'world';

    return { body: `Hello, ${name}!` };
};

app.http('generateBitcoinAddress', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: generateBitcoinAddress
});

I built it and deployed it on docker:

npm run clean
npm run build
docker build --tag YOUR_DOCKER_ID_HERE/azurefunctionsimage:v1.0.0 . docker run -p 8080:80 -it YOUR_DOCKER_ID_HERE/azurefunctionsimage:v1.0.0

It works, I hit the API with Postman using this post request:

curl --location 'http://localhost:8080/api/generateBitcoinAddress' 
--header 'Content-Type: application/json' 
--data 'Chris'

It printed: Hello, Chris! And I ran this locally without Docker as well:

npm run clean
npm run build
func start

To hit this local non-docker API, I altered my Postman command to use port 7071 instead of 8080 and it succeeded. Now is the weird part. I modified the code to do something more complex. I took this test from @cmdcode/tapscript, https://github.com/cmdruid/tapscript/blob/master/test/example/taproot/keyspend.test.ts and converted it into an Azure Function:

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import { keys } from '@cmdcode/crypto-tools';
import { Address, Tap, Tx, Signer } from '@cmdcode/tapscript';

export async function generateBitcoinAddress(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    try {
        const body: any = await request.json();
        const secret_key = body.secret;
        const seckey = keys.get_seckey(secret_key)
        const pubkey = keys.get_pubkey(seckey, true)

        // For key spends, we need to get the tweaked versions
        // of the secret key and public key.
        const [ tseckey ] = Tap.getSecKey(seckey)
        const [ tpubkey ] = Tap.getPubKey(pubkey)

        // A taproot address is simply the tweaked public key, encoded in bech32 format.
        const address = Address.p2tr.fromPubKey(tpubkey, 'regtest')
        console.log('Your address:', address)

        /* NOTE: To continue with this example, send 100_000 sats to the above address.
        You will also need to make a note of the txid and vout of that transaction,
        so that you can include that information below in the redeem tx.
        */ 

        const txdata = Tx.create({
        vin  : [{
            // Use the txid of the funding transaction used to send the sats.
            txid: '1ec5b5403bbc7f26a5d3a3ee30d69166a19fa81b49928f010af38fa96986d472',
            // Specify the index value of the output that you are going to spend from.
            vout: 1,
            // Also include the value and script of that ouput.
            prevout: {
            // Feel free to change this if you sent a different amount.
            value: 100_000,
            // This is what our address looks like in script form.
            scriptPubKey: [ 'OP_1', tpubkey ]
            },
        }],
        vout : [{
            // We are leaving behind 1000 sats as a fee to the miners.
            value: 99_000,
            // This is the new script that we are locking our funds to.
            scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
        }]
        })

        // For this example, we are signing for input 0 of our transaction,
        // using the tweaked secret key.
        const sig = Signer.taproot.sign(tseckey, txdata, 0)

        // Let's add this signature to our witness data for input 0.
        txdata.vin[0].witness = [ sig ]

        // Check if the signature and transaction are valid.
        const isValid = await Signer.taproot.verify(txdata, 0)

         return { status: 200, jsonBody: { isValid: isValid, address: address, signature: sig }};

    } catch (error) {
        context.error(`Error processing request: ${error}`);
        return { status: 500, jsonBody: { error: "Internal server error. Please try again later." }};
    }
};

app.http('generateBitcoinAddress', {
    methods: ['POST'],
    authLevel: 'anonymous',
    handler: generateBitcoinAddress
});  

It worked perfectly when I ran it locally WITHOUT Docker...

npm install u/cmdcode/buff
npm install u/cmdcode/buff-utils\
npm install u/cmdcode/tapscript
npm install u/cmdcode/crypto-tools
npm run clean
npm run build
func start

Then in Postman, I did:

curl --location 'http://localhost:7071/api/generateBitcoinAddress' 
--header 'Content-Type: application/json' 
--data '{ "secret": "ccd54b99acec77d0537b01431579baef998efac6b08e9564bc3047b20ec1bb4c" }'

It worked and produced this output:

{
  "isValid": true,
  "address": "bcrt1pszn3ahyts27pr8zvqf3p0v53y5f0gwgcz85nwrdh2kxw4xxd00mqh3mjcv",
  "signature": {
    "0": 43,
    "1": 98,
    LOTS MORE ARRAY VALUES THAT I WILL OMIT FOR BREVITY
  }
}

But when I took this working Azure Function, and tried to launch it in a docker container, it didn't work:

docker build --tag YOUR_DOCKER_ID_HERE/azurefunctionsimage:v1.0.0 .
docker run -p 8080:80 -it YOUR_DOCKER_ID_HERE/azurefunctionsimage:v1.0.0

I noticed this line in the output of the docker run command that wasn't present in the boilerplate code docker run command output: No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).

So I already knew it would fail because no function is at the API endpoint. But just to make sure it would fail, I altered my Postman command for port 8080:

curl --location 'http://localhost:8080/api/generateBitcoinAddress' 
--header 'Content-Type: application/json' 
--data '{ "secret": "ccd54b99acec77d0537b01431579baef998efac6b08e9564bc3047b20ec1bb4c" }'

And it gave me a 404 error. So what is the problem? Why does my code work locally when I run it with func start, but doesn't work when I deploy it via Docker?

cmdruid commented 1 month ago

Do you know what version of node you are running in the docker container? It should be version 19+ to support the globalThis.crypto interface.

ChristianOConnor commented 1 month ago

Do you know what version of node you are running in the docker container? It should be version 19+ to support the globalThis.crypto interface.

That fixed it! I was using node 18. I replaced FROM mcr.microsoft.com/azure-functions/node:4-node18 with FROM mcr.microsoft.com/azure-functions/node:4-node20 and it works now! Thanks!