This is an api documentation using musig2 and mast for btc. These help to build threshold signature wallets for android. In response to the taproot upgrade, this api also provides builds for taproot normal transactions and threshold wallet transactions.
Step 1. Add the JitPack repository to your build file
allprojects {
repositories {
...
maven {url'https://jitpack.io'}
}
}
Step 2. Add dependencies
dependencies {
implementation'com.github.chainx-org:musig2-android-api:1.7.8'
}
Step 3. Import the musig2bitcoin package
import com.example.musig2bitcoin.Musig2;
import com.example.musig2bitcoin.Mast;
import com.example.musig2bitcoin.Transaction;
The following are functions related to constructing transactions
Construct an original transaction, which is used to calculate the transaction hash and then sign. The entered transaction id and the entered transaction index must correspond one-to-one. The output address and the output quantity must correspond one-to-one. Support op_return, just set amout to 0, and the corresponding address setting needs to be accompanied by information.
Name | Type | Description |
---|---|---|
prev_txs | [String] | List of the original transaction input |
txids | [String] | List of entered transaction ids |
input_indexs | [UInt32] | Input transaction index list |
addresses | [String] | Output address list |
amounts | [UInt64] | List of output quantities |
Return | String | Original transaction text |
-txids and indexes must be equal in length
-addresses and amounts must be equal in length
-Input count must be greater than 0
-Output count must be greater than 0
-Invalid Transaction
-Invalid Tx Input
-Invalid Tx Output
Calculate the transaction hash (sighash). A transaction has multiple inputs, and each input needs to calculate a sighash, and then sign the sighash to get the signature.
Name | Type | Description |
---|---|---|
prev_tx | String | Original transaction input |
tx | String | Result returned by generateRawTx |
input_index | long | Input transaction index |
agg_pubkey | String | When the input is a non-threshold address, fill in ""; when the input is a threshold address, fill in the aggregate public key (getAggPublicKey) |
sigversion | long | When the input is a non-threshold address, fill in 0; when the input is a threshold address, fill in 1 |
protocol | String | Protocol name,btc:"", brc20: "brc20", runes:"runes" |
Return | String | Current input transaction hash |
-Compute Sighash Fail
For non-threshold addresses, use the above sighash and this function to calculate the signature
Name | Type | Description |
---|---|---|
message | String | The message to be signed, which is the sighash calculated above |
privkey | String | Signer's private key |
Return | String | Schnorr Signature |
-Invalid Signature
The unsigned transaction text generated from generateRawTx
, carrying custom additional information, is not a valid transaction text. The purpose of getUnsignedTx
is to generate a valid unsigned transaction text that can be parsed by the BTC network.
Name | Type | Description |
---|---|---|
tx | String | Unsigned transaction text with additional information |
Return | String | Generate valid unsigned transaction text |
Invalid Transaction
When the address is not threshold, use this function to assemble the signature generated by generateSchnorrSignature
into the original transaction generated by generateRawTx
. Each input must be signed once, so multiple inputs must be assembled multiple times.
Name | Type | Description |
---|---|---|
tx | String | Original transaction calculated by generateRawTx |
signature | String | Single Schnorr signature |
input_index | long | Input transaction index |
Return | String | Return the assembled transaction |
-Construct Tx Fail
When the threshold address is used, use this function to assemble the aggregated signature generated by Musig2
into the original transaction generated by generateRawTx
. Each input must be signed once, so multiple inputs must be assembled multiple times.
Name | Type | Description |
---|---|---|
tx | String | Original transaction calculated by generateRawTx |
agg_signature | String | Musig2 aggregated signature |
agg_pubkey | String | Musig2 aggregate public key |
control | String | Proof generated by Mast |
input_index | long | Input transaction index |
protocol | String | Protocol name,btc:"", brc20: "brc20", runes:"runes" |
Return | String | Return the assembled transaction |
-Construct Tx Fail
Use address to generate scirpt_pubkey, support all address formats.
Name | Type | Description |
---|---|---|
addr | String | Address |
Return | String | scirpt_pubkey |
-Invalid Address
Generate spend outputs. Use createTaprootWithdrawTx
in Chainx.
Name | Type | Description |
---|---|---|
prev_txs | String[] | Input transaction array |
input_indexs | long[] | Input transaction index array |
Return | String | Serialized spend outputs |
-Invalid Spent Outputs
Generate address
Name | Type | Description |
---|---|---|
pubkey | String | Public Key |
network | String | Bitcoin network type, supports "mainnet", "signet", "testnet", "regtest" |
Return | String | Address |
-Invalid Public Bytes
The following are functions related to aggregated signatures and aggregated public keys
Generate private key from mnemonic phrase and password
Name | Type | Description |
---|---|---|
phrase | String | mnemonic phrase |
pd_passphrase | String | Password |
Return | String | Private Key |
-Construct Secret Key
Generate public key from private key
Name | Type | Description |
---|---|---|
private | String | Private Key |
Return | String | Public Key |
-Null KeyPair Pointer
-Normal Error
Musig2 generates the state of the first round.
Name | Type | Description |
---|---|---|
Return | OpaquePointer? | First round status |
-null pointer
Generate messages through the first round of status for delivery to other participants
Name | Type | Description |
---|---|---|
state | OpaquePointer? | First round state |
Return | String | First round of news |
-Null Round1 State Pointer
-Normal Error
Serialize the first round of state
Name | Type | Description |
---|---|---|
state | OpaquePointer? | First round state |
Return | String | Serialization result |
-Null Round1 State Pointer
-Encode Fail
Deserialize the first round of state
Name | Type | Description |
---|---|---|
round1_state | String | The output value of encodeRound1State |
Return | OpaquePointer? | First round status |
-null pointer
Generate the second round of messages
Name | Type | Description |
---|---|---|
state | long | The output value of encodeRound1State |
msg | String | The message to be signed, usually the return value of getSighash |
priv | String | Current participant private key |
pubkeys | String[] | Public keys of all multi-signature participants |
received_round1_msg | String[] | The first round of messages received from other multi-signature participants |
Return | String | Second round of messages |
-null pointer
Return the result of the aggregated signature
Name | Type | Description |
---|---|---|
round2_msg | String | The second round of messages from all participants |
Return | String | Signature Result |
-Normal Error
-Null Round2 State Pointer
Generate aggregate public key
Name | Type | Description |
---|---|---|
pubkeys | String[] | List of public keys to be aggregated |
Return | String | Aggregate Public Key |
-Normal Error
The following is the function related to generating the threshold address and proof
Generate threshold pubkey
Name | Type | Description |
---|---|---|
pubkeys | String[] | List of all public keys |
threshold | byte | Threshold |
Return | String | Aggregate Public Key |
-Invalid Public Bytes
Generate proof
Name | Type | Description |
---|---|---|
pubkeys | String[] | List of all public keys |
threshold | byte | Threshold |
aggPubkey | String | The aggregate public key of this multi-signature participant |
protocol | String | Protocol name,btc:"", brc20: "brc20", runes:"runes" |
Return | String | proof |
-Invalid Public Bytes
The following examples provide: constructing a non-threshold address, the cost of a non-threshold address, constructing a threshold signature address, and a threshold signature address cost. The complete code can be viewed in MainActivity.java.
-
Pass in the mnemonic phrase and password to generate a private key
String private0 = getMyPrivkey(PHRASE0, "")
-
Generate public key
String pubkey0 = getMyPubkey(private0)
-
Generate Address
String addr0 = getMyAddress(pubkey0, "signet");
-
Create an unsigned transaction via
generateRawTx
. Txids and indexes are used to construct all the inputs of the transaction, and a txid and an index are used to locate the only unspent output. Below prev_txs, txids and input_indexs have the same length and correspond one to one. Addresses and amounts are used to construct all the outputs of the transaction. An adddress and an amount indicate how many coins are sent to an address. There is no order requirement for adddress, just a one-to-one correspondence between amounts. And here1f8e0f7dfa37b184244d022cdf2bc7b8e0bac8b52143ea786fa3f7bbe049eeae
and1
uniquely determine an unspent output. The address of this unspent output is a non-threshold address.35516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38
representsop_return
, and its corresponding amout is 0.tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw
is the recipient's address and100000
is the transfer amount.tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68
is the change address, and400000
is the change amount. Refer to Calculation of Handling Fee and Change Balance for the calculation method.String [] prev_txs = new String [] { "020000000001014be640313b023c3c731b7e89c3f97bebcebf9772ea2f7747e5604f4483a447b601000000000000000002a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bbc027090000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01404dc68b31efc1468f84db7e9716a84c19bbc53c2d252fd1d72fa6469e860a74486b0990332b69718dbcb5acad9d48634d23ee9c215ab15fb16f4732bed1770fdf00000000"}; String[] txids = new String[]{"1f8e0f7dfa37b184244d022cdf2bc7b8e0bac8b52143ea786fa3f7bbe049eeae"}; long[] input_indexs = new long[]{1}; String [] addresses = new String [] { "tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw", "35516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38", "tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68"}; long[] amounts = new String[]{100000, 0, 400000}; String base_tx = Transaction.generateRawTx(prev_txs, txids, input_indexs, addresses, amounts); String final_tx = base_tx;
-
Sign the output to be spent. To sign the UTXO to be spent, first calculate the sighash of the unspent output. The signature is to sign the sighash.
prev_tx and input_index are used to locate the output to be spent, agg_pubkey fills in the string
""
for non-threshold signature addresses, sigversion fills in 0 for non-threshold signature addresses, tx is the currently constructed transaction .**Note that when calculating sighash, always use the abovegenerateRawTx
to construct the result and cannot be changed. **String sighash = Transaction.getSighash(base_tx, txids[i], input_indexs[0], "", 0, "");
After calculating the sighash, use the private key to sign it. The message refers to sighash, and the privkey refers to the private key.
String schnorr_signature = Transaction.generateSchnorrSignature(sighash, private_key);
-
Assemble the above signature into the transaction. tx is the current transaction to be constructed, and txid and input_index are still used to locate the input corresponding to the signature in tx.
String final_tx = Transaction.buildTaprootTx(base_tx, schnorr_signature, txids[i], input_indexs[i]);
**Note that if there are multiple inputs in tx, you need to repeat Step2 and Step3 to sign each output and add it to tx, as shown in the following for loop:. **
-
Generate a 2-of-3 threshold signature address as follows. First, pass in the public keys and thresholds of all participants to generate the threshold public keys.
String threshold_pubkey = Mast.generateThresholdPubkey(new String[]{publicA, publicB, publicC}, (byte) 2, "");
-
Then encode the public key into an address to get the threshold address
String threshold_address = Transaction.getMyAddress(threshold_pubkey, "signet");
-
Create an unsigned transaction via
generateRawTx
. Txids and indexes are used to construct all the inputs of the transaction, and a txid and an index are used to locate the only unspent output. Below prev_txs, txids and input_indexs have the same length and correspond one to one. Addresses and amounts are used to construct all the outputs of the transaction. An adddress and an amount indicate how many coins are sent to an address. There is no order requirement for adddress, just a one-to-one correspondence between amounts. Here8e5d37c768acc4f3e794a10ad27bf0256237c80c22fa67117e3e3e1aec22ea5f
and0
uniquely determine an unspent output. Note that the address of this unspent output is a threshold address.tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68
is the recipient's address,50000
is the transfer amount.tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw
is the change address, and40000
is the change amount. Of course, you can also bringop_return
here. The calculation method refers to Calculation of handling fee and change balanceprev_txs = new String [] { "02000000000101aeee49e0bbf7a36f78ea4321b5c8bae0b8c72bdf2c024d2484b137fa7d0f8e1f01000000000000000003a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bb0000000000000000326a3035516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38801a060000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01409e325889515ed47099fdd7098e6fafdc880b21456d3f368457de923f4229286e34cef68816348a0581ae5885ede248a35ac4b09da61a7b9b90f34c200872d2e300000000"}; txids = new String[]{"8e5d37c768acc4f3e794a10ad27bf0256237c80c22fa67117e3e3e1aec22ea5f"}; input_indexs = new long[]{0}; addresses = new String[]{"tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68","tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwas}; amounts = new long[]{50000, 40000}; String base_tx = Transaction.generateRawTx(prev_txs, txids, input_indexs, addresses, amounts); String final_tx = base_tx;
-
Sign the output to be spent. To sign the UTXO to be spent, first calculate the sighash of the unspent output, and the signature is to sign the sighash.
The prev_tx and input_index are used to locate the output to be spent. Agg_pubkey fills in the empty string aggregate public key for the threshold signature address. The following is the aggregate public key of B and C for two people, then fill in the aggregate public key of B and C. sigversion fills in 1 for the threshold signature address, and tx is the currently constructed transaction.
Calculate sighash
String pubkey_bc = Musig2.getAggPublicKey(new String[]{pubkey_b, pubkey_c}) sighash = Transaction.getSighash(base_tx, txids[i], input_index[i], pubkey_bc, 1, "");
Calculate signature: After calculating sighash, B and C use Musig2 to aggregate signatures. The signed message is sighash.
String round1_state0 = Musig2.getRound1State() long state_str = Musig2.encodeRound1State(round1_state0); round1_state0 = Musig2.decodeRound1State(state_str) String round1_state1 = Musig2.getRound1State() String round1_msg0 = Musig2.getRound1Msg(round1_state0) String round1_msg1 = Musig2.getRound1Msg(round1_state1) String round2_msg0 = Musig2.getRound2Msg(round1_state0, sighash, private_b, new String[]{pubkey_b, pubkey_c}, new String[]{round1_msg1}) String round2_msg1 = Musig2.getRound2Msg(round1_state1, sighash, private_c, new String[]{pubkey_b, pubkey_c}, new String[]{round1_msg0}) String multi_signature = Musig2.getAggSignature(new String[]{round2_msg0, round2_msg1})
The following is a detailed introduction to the above-mentioned Musig2 multi-signature process, divided into the following steps:
-
Generate the state of the first round
long round1_state0 = getRound1State()
-
Obtain the first round of messages through the first round of status and pass them to other signing participants.
String round1_msg0 = getRound1Msg(round1_state0)
-
Get the first round of messages from other signing participants, generate the second round of messages, and pass them to other participants.
received_round1_msg
is the first round of messages received from other participants.pubkeys
are the public keys of all participants.msg
is the message to be signed.state
is the state of the first round.priv
is the signer's private key.String round2_msg0 = getRound2Msg(round1_state0, sighash, private_b, new String[]{pubkey_b, pubkey_c}, new String[]{round1_msg1})
-
Use the second round of messages from all participants to generate aggregate signatures.
round2_msg
is the second round of messages from all participants.String multi_signature = getAggSignature(new String[]{round2_msg0, round2_msg1})
Calculate proof: The cost of threshold signature not only requires signature, but also calculates proof. It is necessary to pass in the public key of everyone, the threshold, and the aggregate public key of participants B and C of this signature.
String control_block = Msat.generateControlBlock(new String[]{pubkey_a, pubkey_b, pubkey_c}, (byte) 2, pubkey_bc, "")
-
-
Assemble the above signature for transaction. tx is the current transaction to be constructed, agg_signature is the aggregated signature of B and C, agg_pubkey is the aggregated public key of B and C, txid and input_index are still used to locate the input corresponding to the signature in tx, and the unspent output corresponding to txid and input_index The second step is corresponding.
String final_tx = Transaction.buildThresholdTx(base_tx, multi_signature, pubkey_bc, control_block, txids[i], input_indexs[i]);
Note that if there are multiple inputs in tx, you need to repeat Step2 and Step3 to sign each output and add it to tx, as shown in the following for loop:
Background: A wants to transfer money to B 2BTC
, C 3BTC
-
Find all unspent transaction txids and balances through the address of A, and sort them from largest to smallest, assuming it is
[(txid1, 4), (txid2, 2), (tixd3, 1), (tixd4, 1) ]
. -
Accumulate the txids and balance list and find the txid that is greater than the output amount 2+3=5, that is, txid2. If it is not found, it will return that the transfer is not allowed.
-
Extend one bit from txid2 backward, using
[(txid1, 4), (txid2, 2), (tixd3, 1)]
as input. If txid2 is the last one, use[(txid1, 4), (txid2, 2)]
as input. -
Use the number of inputs and outputs and the following formula to estimate the number of transaction bytes:
Estimation of the number of bytes spent by non-threshold addresses
105 + 58 * input_count(threshold_address) + 43 * output_count
input_count(taproot_address)
represents the number of input txid when the non-threshold address is spentEstimation of the number of bytes of the threshold address
105 + 141 * input_count(threshold_address) + 43 * output_count
input_count(threshold_address)
represents the number of input txid when the threshold address is spent -
Multiply the number of bytes by the current
FEE RATES
to get the transaction fee. -
Enter
total amount-(total amount of output + handling fee)
to getchange amount
. If it is negative, there is no change (that is, the change address and amount are not filled in the output list), and the transaction fee becomesTotal input amount-Total amount to output
.
-
Convert the address into the locked script script_pubkey output in the Bitcoin transaction
String script_pubkey = Transaction.getScriptPubkey("tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw")
-
Pass in a set of transactions and a corresponding set of indexes to locate a set of outputs to be spent
String spend_outputs = Transaction.generateSpentOutputs(prev_txs, input_indexs)
-
Extracts a valid unsigned original transaction from the original unsigned transaction generated by
generateRawTx
.String unsigned_tx = Transaction.getUnsignedTx(base_tx);