How to use Web3Auth MPC CoreKit SDK with Bitcoin Taproot
As Bitcoin adoption continues to grow, ensuring secure and user-friendly key management is more important than ever. Web3Auth MPC CoreKit provides a robust solution by leveraging multi-party computation (MPC) to enhance security while maintaining a seamless user experience.
This guide walks you through setting up and using Web3Auth MPC CoreKit specifically for Bitcoin Taproot, demonstrating how to integrate Web3Auth’s decentralized key infrastructure infrastructure to sign Bitcoin transactions. By the end of this guide, you’ll be able to securely generate and manage Bitcoin keys, sign transactions, and interact with the Bitcoin network without compromising on security or usability.
Whether you’re a developer looking to integrate Web3Auth MPC CoreKit into your Bitcoin applications or simply exploring MPC-based key management, this guide provides a hands-on approach to getting started. Let’s dive in!
Prerequisites
- Web3Auth Dashboard: If you haven't already, sign up on the Web3Auth platform. It is free and gives you access to the Web3Auth's base plan. Head to Web3Auth's documentation page for detailed instructions on setting up the Web3Auth Dashboard.
- Web3Auth MPC CoreKit SDK: This guide assumes that you already know how to integrate the MPC CoreKit SDK in your project and able to set up the login flow. If not, you can learn how to integrate Web3Auth MPC Core Kit SDK in your web app.
TLDR;
-
Web3Auth MPC CoreKit: Initialize the CoreKit instance and set up the login flow.
-
Bitcoin Signer: Create a BitcoinJS-compatible signer for BIP340 Schnorr signatures.
-
Bitcoin Operations: Implement Bitcoin-specific operations like address generation and transaction signing.
-
Usage Guide: Learn how to use the application to interact with Bitcoin Taproot.
Get a clone of the example repository to follow along with the guide.
Installation
To get started, install the necessary dependencies for the Web3Auth MPC CoreKit Bitcoin example:
npm install @web3auth/mpc-core-kit @toruslabs/tss-frost-lib-bip340
Initialization
Before interacting with Web3Auth MPC CoreKit, we need to initialize it. This is done in the
App.tsx
file, where we configure the CoreKit instance with the necessary parameters.
Setting Up Web3Auth MPC CoreKit
In the snippet below, we create an instance of Web3Auth MPC CoreKit with the required configurations:
import { Web3AuthMPCCoreKit, WEB3AUTH_NETWORK } from "@web3auth/mpc-core-kit";
import { tssLibFrostBip340 } from "@toruslabs/tss-frost-lib-bip340";
let coreKitInstance: Web3AuthMPCCoreKit;
if (typeof window !== "undefined") {
coreKitInstance = new Web3AuthMPCCoreKit({
web3AuthClientId, // Your Web3Auth Client ID, get it from the Web3Auth dashboard
web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, // Web3Auth's DEVNET environment
storage: window.localStorage, // Uses localStorage for persisting user session
manualSync: true, // Requires manual syncing of key shares
tssLib: tssLibFrostBip340, // Uses Frost-BIP340 for threshold signature scheme (TSS)
});
}
The parameters for initializing the Web3Auth MPC CoreKit instance are as follows:
web3AuthClientId
is your unique Web3Auth Client ID.web3AuthNetwork
specifies the Web3Auth network (DEVNET in this case).storage
determines where session data is stored (using localStorage here).manualSync
enables manual synchronization of key shares for added control.tssLibFrostBip340
defines the threshold signature scheme (TSS) library, specifically Frost-BIP340, optimized for Bitcoin Taproot.
Initializing the CoreKit Instance
Once the instance is created, we initialize it inside a useEffect hook:
useEffect(() => {
const init = async () => {
setIsLoading(true);
await coreKitInstance.init(); // Initializes the CoreKit instance
setCoreKitStatus(coreKitInstance.status); // Updates state with CoreKit status
setIsLoading(false);
};
init();
}, []);
Authentication
The project uses Firebase for authentication, allowing users to log in securely using their Google
accounts. You can choose any authentication provider that suits your needs, but for this example, we
using Firebase for its ease of integration. The login function, implemented in App.tsx
, handles
this authentication flow and integrates with Web3Auth MPC CoreKit.
Logging in with Google
The login function follows these steps to authenticate users and establish a Web3Auth session:
- Login with Google
- Firebase Config
import { signInWithGoogle } from "./firebase";
const login = async () => {
try {
if (!coreKitInstance) {
throw new Error("CoreKit instance not initialized");
}
const loginRes = await signInWithGoogle(); // Sign in using Firebase Google authentication
const idToken = await loginRes.user.getIdToken(true); // Retrieve the Firebase ID token
const parsedToken = parseToken(idToken); // Decode the token to extract user details
const idTokenLoginParams = {
verifier, // The verifier configured for authentication
verifierId: parsedToken.sub, // Unique user identifier from the token
idToken, // The JWT token to be used for authentication
} as JWTLoginParams;
await coreKitInstance.loginWithJWT(idTokenLoginParams); // Authenticate with Web3Auth using the JWT token
if (coreKitInstance.status === COREKIT_STATUS.LOGGED_IN) {
await coreKitInstance.commitChanges(); // Required for new accounts to persist changes
}
if (coreKitInstance.status === COREKIT_STATUS.REQUIRED_SHARE) {
setShowRecoveryOptions(true);
uiConsole(
"More shares required. Please enter your backup/device factor key or reset your account. [Warning: Resetting is irreversible, use with caution]",
);
}
setCoreKitStatus(coreKitInstance.status); // Update the application state with the CoreKit status
} catch (err) {
uiConsole(err); // Log any errors to the console
}
};
import { initializeApp } from "firebase/app";
const app = initializeApp(firebaseConfig);
const firebaseConfig = {
apiKey: "AIzaSyB0nd9YsPLu-tpdCrsXn8wgsWVAiYEpQ_E",
authDomain: "web3auth-oauth-logins.firebaseapp.com",
projectId: "web3auth-oauth-logins",
storageBucket: "web3auth-oauth-logins.appspot.com",
messagingSenderId: "461819774167",
appId: "1:461819774167:web:e74addfb6cc88f3b5b9c92",
};
export const signInWithGoogle = async (): Promise<UserCredential> => {
try {
const auth = getAuth(app);
const googleProvider = new GoogleAuthProvider();
const res = await signInWithPopup(auth, googleProvider);
console.log(res);
return res;
} catch (err) {
console.error(err);
throw err;
}
};
The login function performs the following steps:
- Checks the CoreKit Instance – Ensures coreKitInstance is initialized before proceeding.
- Sign in with Google – Calls
signInWithGoogle()
to authenticate the user via Firebase. - Retrieve ID Token – After login, the Firebase ID token is fetched.
- Parse Token – Decodes the ID token to extract user information, including the sub field (a unique user identifier) used during login with Web3Auth.
- Authenticate with Web3Auth – Calls
loginWithJWT()
, passing the Firebase ID token(JWT) to Web3Auth for authentication. - Commit Changes (If Needed) – If the user is logging in for the first time,
commitChanges()
ensures key shares are properly stored. - Handle Recovery (If Needed) – If Web3Auth requires additional shares to reconstruct the key, the UI prompts the user for recovery options.
- Update Application State – The current CoreKit status is stored for UI updates.
Bitcoin Signer
To enable Bitcoin Taproot signing with Web3Auth MPC CoreKit, we need to create a
BitcoinJS-compatible signer that works with the BIP340 Schnorr signature scheme. The function
createBitcoinJsSignerBip340
accomplishes this by deriving a tweaked Taproot public key and
implementing the necessary signing methods.
Bitcoin supports multiple signature schemes, including the Schnorr signature scheme introduced in BIP340. This scheme is essential for Taproot, a Bitcoin soft fork that improves privacy and efficiency. The following code demonstrates how to create a BIP340-compatible signer using Web3Auth’s MPC CoreKit.
Bitcoin also has other form addresses like P2PKH, P2SH, P2WPKH, P2WSH, etc. For this example, we are using the Taproot address (P2TR) for demonstration purposes. Learn more about Bitcoin address types.
Implementing the Bitcoin Signer
The following code defines the BIP340 signer using Web3Auth’s MPC CoreKit:
import { secp256k1 } from "@tkey/common-types";
import { Web3AuthMPCCoreKit } from "@web3auth/mpc-core-kit";
import { networks, SignerAsync } from "bitcoinjs-lib";
import * as bitcoinjs from "bitcoinjs-lib";
import ECPairFactory from "ecpair";
import ecc from "@bitcoinerlab/secp256k1";
import BN from "bn.js";
const ECPair = ECPairFactory(ecc);
export function createBitcoinJsSignerBip340(props: { coreKitInstance: Web3AuthMPCCoreKit; network: networks.Network }): SignerAsync {
const bufPubKey = props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true);
const xOnlyPubKey = bufPubKey.subarray(1, 33);
const keyPair = ECPair.fromPublicKey(bufPubKey);
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", xOnlyPubKey);
const tweakedChildNode = keyPair.tweak(tweak);
const pk = tweakedChildNode.publicKey;
return {
sign: async (msg: Buffer) => {
let sig = await props.coreKitInstance.sign(msg);
return sig;
},
signSchnorr: async (msg: Buffer) => {
const keyTweak = new BN(tweak);
let sig = await props.coreKitInstance.sign(msg, { keyTweak });
return sig;
},
publicKey: pk,
network: props.network,
};
}
The createBitcoinJsSignerBip340 function performs the following steps:
1️⃣ Import Required Libraries
The function imports:
- BitcoinJS (bitcoinjs-lib) - Provides Bitcoin transaction utilities.
- ECPair (ecpair) – Used for public key derivation and tweaking.
- BN.js (bn.js) – Handles large number computations (for Taproot key tweaking).
- Web3Auth MPC CoreKit (
@web3auth/mpc-core-kit
) – Enables multi-party computation for private keys.
npm install bitcoinjs-lib ecpair bn.js
2️⃣ Retrieve the Public Key from CoreKit
const bufPubKey = props.coreKitInstance.getPubKeyPoint().toSEC1(secp256k1, true);
const xOnlyPubKey = bufPubKey.subarray(1, 33);
- The public key is fetched from the Web3Auth MPC CoreKit instance.
- It is converted to SEC1 format and extracted as an x-only public key (removing the first byte). The x-only public key is used for Taproot addresses.
3️⃣ Apply Taproot Tweak for BIP340 Compatibility
const keyPair = ECPair.fromPublicKey(bufPubKey);
const tweak = bitcoinjs.crypto.taggedHash("TapTweak", xOnlyPubKey);
const tweakedChildNode = keyPair.tweak(tweak);
const pk = tweakedChildNode.publicKey;
- Taproot keys must be tweaked using a tagged hash (TapTweak) to ensure scriptless scripts work correctly.
- The tweaked key is derived using ECPair.tweak(tweak).
4️⃣ Implement Signing Functions
sign: async (msg: Buffer) => {
let sig = await props.coreKitInstance.sign(msg);
return sig;
}
- sign method – Uses Web3Auth MPC CoreKit to sign standard Bitcoin transactions.
signSchnorr: async (msg: Buffer) => {
const keyTweak = new BN(tweak);
let sig = await props.coreKitInstance.sign(msg, { keyTweak });
return sig;
},
- signSchnorr method – Signs transactions using the Schnorr signature scheme (BIP340).
- The public key is tweaked using BN.js before signing.
5️⃣ Return the BitcoinJS-Compatible Signer
return {
publicKey: pk,
network: props.network,
};
- The signer returns the tweaked Taproot public key and the associated Bitcoin network configuration.
Bitcoin Operations
The BitcoinComponent.tsx
file implements Bitcoin-specific operations, allowing users to:
- showAddress – Display the Taproot (BIP340) Bitcoin address.
- showBalance – Fetch and display the balance for the generated Bitcoin address.
- signAndSendTransaction – Sign and optionally broadcast Bitcoin transactions using Web3Auth MPC CoreKit.
This component integrates BitcoinJS (bitcoinjs-lib), Schnorr signatures (@bitcoinerlab/secp256k1
),
and Web3Auth MPC for Taproot-compatible signing.
import { Web3AuthMPCCoreKit } from "@web3auth/mpc-core-kit";
import { useEffect, useState } from "react";
import ecc from "@bitcoinerlab/secp256k1";
import { networks, Psbt, payments, SignerAsync } from "bitcoinjs-lib";
import * as bitcoinjs from "bitcoinjs-lib";
import { createBitcoinJsSignerBip340 } from "./BitcoinSigner";
import axios from "axios";
import { BlurredLoading } from "./Loading";
The BitcoinComponent.tsx file imports the necessary libraries and components for Bitcoin operations:
1️⃣ Dependencies and Libraries
- bitcoinjs-lib – Handles Bitcoin transactions and scripts.
@bitcoinerlab/secp256k1
– Implements Schnorr signatures for BIP340.- axios – Fetches data from external Bitcoin APIs (Blockstream testnet).
- Web3AuthMPCCoreKit – Enables MPC-based key management and signing.
npm install @bitcoinerlab/secp256k1 axios
2️⃣ Initialize BitcoinJS with Schnorr Support
bitcoinjs.initEccLib(ecc);
- This ensures BitcoinJS uses Schnorr signing for Taproot transactions.
3️⃣ Create a Bitcoin Address
const getAddress = (bip340Signer: SignerAsync, network: networks.Network): string | undefined => {
return payments.p2tr({ pubkey: bip340Signer.publicKey.subarray(1, 33), network }).address;
};
- Converts the BIP340 public key into a Taproot (P2TR) address.
4️⃣ Fetch Unspent Transaction Outputs (UTXOs)
const fetchUtxos = async (address: string) => {
try {
const response = await axios.get(`https://blockstream.info/testnet/api/address/${address}/utxo`);
return response.data.filter((utxo: { status: { confirmed: boolean } }) => utxo.status.confirmed);
} catch (error) {
console.error("Error fetching UTXOs:", error);
return [];
}
};
- Calls Blockstream API to get UTXOs (spendable funds) for a given Bitcoin address.
- Filters for confirmed transactions only.
5️⃣ Sign and Send Taproot Transactions
const signAndSendTransaction = async (send: boolean = false) => {
if (!bip340Signer) {
uiConsole("BIP340 Signer not initialized yet");
return;
}
setIsLoading(true);
try {
const account = payments.p2tr({ pubkey: bip340Signer.publicKey.subarray(1, 33), network: bitcoinNetwork });
const utxos = await fetchUtxos(account.address!);
if (!utxos.length) {
throw new Error("No UTXOs found for this address");
}
const utxo = utxos[0];
const feeResponse = await axios.get("https://blockstream.info/testnet/api/fee-estimates");
const maxFee = Math.max(...Object.values(feeResponse.data as Record<string, number>));
const fee = Math.ceil(maxFee * 1.2);
if (utxo.value <= fee) {
throw new Error(`Insufficient funds: ${utxo.value} satoshis <= ${fee} satoshis (estimated fee)`);
}
const sendAmount = amount ? parseInt(amount) : utxo.value - fee;
const psbt = new Psbt({ network: bitcoinNetwork });
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: account.output!,
value: utxo.value,
},
tapInternalKey: bip340Signer.publicKey.subarray(1, 33),
});
psbt.addOutput({
address: receiverAddr || account.address!,
value: sendAmount,
});
uiConsole("Signing transaction...");
await psbt.signInputAsync(0, bip340Signer);
const isValid = psbt.validateSignaturesOfInput(0, BTCValidator);
if (!isValid) {
throw new Error("Transaction signature validation failed");
}
const signedTransaction = psbt.finalizeAllInputs().extractTransaction().toHex();
uiConsole("Signed Transaction:", signedTransaction, "Copy the above into https://blockstream.info/testnet/tx/push");
if (send) {
const txid = await handleSendTransaction(signedTransaction);
uiConsole("Transaction sent. TXID:", txid);
}
} catch (error) {
console.error(`Error in signTaprootTransaction:`, error);
uiConsole("Error:", (error as Error).message);
} finally {
setIsLoading(false);
}
};
- Fetches UTXOs for the Taproot address.
- Estimates fees dynamically using Blockstream API.
- Signs the transaction using the MPC-based BIP340 signer.
- Validates the signature before broadcasting.
- Finalizes and extracts the signed transaction hex.
- Optionally sends the transaction via Blockstream API.
6️⃣ Display Bitcoin Address and Balance
const showAddress = async () => {
if (!bip340Signer) {
uiConsole("Signer not initialized yet");
return;
}
setIsLoading(true);
try {
const address = getAddress(bip340Signer, bitcoinNetwork);
if (address) {
uiConsole(`Address:`, address);
} else {
uiConsole("Invalid address");
}
} finally {
setIsLoading(false);
}
};
- Displays the generated Taproot address.
const showBalance = async () => {
if (!bip340Signer) {
uiConsole("Signer not initialized yet");
return;
}
setIsLoading(true);
try {
const address = getAddress(bip340Signer, bitcoinNetwork);
if (!address) {
uiConsole("Invalid address");
return;
}
const utxos = await fetchUtxos(address);
const balance = utxos.reduce((acc: any, utxo: { value: any }) => acc + utxo.value, 0);
uiConsole(` Balance:`, balance, "satoshis");
} catch (error) {
console.error(`Error fetching balance for address:`, error);
uiConsole(`Error fetching balance for address:`, (error as Error).message);
} finally {
setIsLoading(false);
}
};
- Fetches and displays the balance in satoshis by summing UTXOs.
7️⃣ Component UI
- Allows users to:
- Enter a receiver Bitcoin address and amount.
- Show the Taproot address.
- Check balance.
- Sign transactions.
- Send transactions to the Bitcoin network.
<button onClick={() => showAddress()} className="card taproot-color">
Show Taproot Address
</button>
<button onClick={() => showBalance()} className="card taproot-color">
Show Taproot Balance
</button>
<button onClick={() => signAndSendTransaction()} className="card taproot-color">
Sign Taproot Transaction
</button>
<button onClick={() => signAndSendTransaction(true)} className="card taproot-color">
Send Taproot Transaction
</button>
Usage Guide
-
Login: Click the "Login" button to authenticate using Firebase.
-
View Addresses: Use the "Show Taproot Address" button to display the Taproot Bitcoin address.
-
Check Balance: Click on "Show Taproot Balance" to view the balance for the Taproot address.
-
Send Transactions:
- Enter the receiver's address and amount in satoshi.
- Click "Sign Taproot Transaction" to sign a transaction.
- Use "Send Taproot Transaction" to sign and send the transaction.
-
Enable MFA: Click the "Enable MFA" button to enable Multi-Factor Authentication.
-
Logout: Use the "Log Out" button to end your session.
Important Notes
- This is a testnet implementation. Use a faucet to get testnet BTC.
- The project uses BlockStream's API for transaction broadcasting, which is not recommended for production use.
- Be cautious with the "Reset Account" functionality, as it will clear all metadata associated with your account.
Customization
To customize the project for your needs:
- Replace the
web3AuthClientId
inApp.tsx
with your own client ID from the Web3Auth dashboard. - Modify the
firebaseConfig
inApp.tsx
if you want to use your own Firebase project. - Customize the UI components in
BitcoinComponent.tsx
to match your design requirements.
Resources
Conclusion
This guide provides an overview of the Web3Auth MPC CoreKit Bitcoin Example. It demonstrates how to integrate secure authentication with Bitcoin functionality, allowing for a range of operations from address generation to transaction signing and sending.