Skip to content

Latest commit

 

History

History
658 lines (412 loc) · 25 KB

README.CN.md

File metadata and controls

658 lines (412 loc) · 25 KB

Overview

这是一个使用 musig2 和 mast 的 api 文档, 有助于为 ios 构建阈值签名钱包。 为了应对taproot升级,这个api还提供了taproot普通交易和门限签名交易的构建。

Dependencies

Step 1. 项目导入

File>Add Packages>Github search https://github.com/chainx-org/musig2-ios-api . The current version is 2.7.0.

Step 2. 导入使用

import Musig2Bitcoin

Api

Construct Transaction

下面是构造交易相关的函数


generateRawTx(prev_txs, txids, input_indexs, addresses, amounts)

说明

构建一个原始的交易,用于下面计算交易哈希然后签名。输入的交易原文,交易id和输入的交易索引必须一一对应。输出的地址和输出的数量必须一一对应。支持op_return,只需将amout设置为0,相应的address设置需要附带的信息即可。

参数和返回值

Name Type Description
prev_txs [String] 输入的交易原文列表
txids [String] 输入的交易id列表
input_indexs [UInt32] 输入的交易索引列表
addresses [String] 输出的地址列表
amounts [UInt64] 输出的数量列表
Return String 初始的交易原文

返回错误

  • txids and indexs 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

getSighash(tx, txid, input_index, agg_pubkey, sigversion, proto)

说明

计算交易哈希(sighash)。一笔交易有多个输入,每个输入都需计算一个sighash,然后对该sighash进行签名得到signature。

参数和返回值

Name Type Description
tx String generateRawTx返回的结果
txid String 输入的交易id
input_index UInt32 输入的交易索引
agg_pubkey String 输入是非门限地址时,填入"";门限地址时填入聚合公钥(getAggPublicKey)
sigversion UInt32 输入是非门限地址时,填入0;输入是门限地址时,填入1;
proto String 协议名称,btc:"", brc20: "brc20", runes:"runes"
Return String 当前输入的交易哈希

返回错误

  • Compute Sighash Fail

generateSchnorrSignature(message, privkey)

说明

非门限地址时,利用上述sighash和该函数计算签名

参数和返回值

Name Type Description
message String 待签名的消息,即上面计算出来的sighash
privkey String 签名者的私钥
Return String Schnorr签名

返回错误

  • Invalid Signature

buildTaprootTx(tx, signature, input_index)

说明

非门限地址时,利用该函数将generateSchnorrSignature生成的签名组装进generateRawTx生成的原始交易。每一个输入都要进行一次签名,因此多个输入要组装多次。

参数和返回值

Name Type Description
tx String generateRawTx计算出的原始交易
signature String 单个Schnorr签名
input_index UInt32 输入的交易索引
Return String 返回组装后的交易

返回错误

  • Construct Tx Fail

buildThresholdTx(tx, agg_signature, agg_pubkey, control, input_index, proto)

说明

门限地址时,利用该函数将Musig2生成的聚合签名组装进generateRawTx生成的原始交易。每一个输入都要进行一次签名,因此多个输入要组装多次。

参数和返回值

Name Type Description
tx String generateRawTx计算出的原始交易
agg_signature String Musig2聚合签名
agg_pubkey String Musig2聚合公钥
control String Mast生成的proof
input_index UInt32 输入的交易索引
proto String 协议名称,btc:"", brc20: "brc20", runes:"runes"
Return String 返回组装后的交易

返回错误

  • Construct Tx Fail

getScriptPubkey(addr)

说明

利用地址生成scirpt_pubkey,支持所有的地址格式。

参数和返回值

Name Type Description
addr String 地址
Return String scirpt_pubkey

返回错误

  • Invalid Address

generateSpentOutputs(prev_txs, input_indexs)

说明

生成spend outputs。使用在Chainx的createTaprootWithdrawTx.

参数和返回值

Name Type Description
prev_txs [String] 输入交易数组
input_indexs [UInt32] 输入交易索引数组
Return String 序列化的spend outputs

返回错误

  • Invalid Spent Outputs

Musig2

下面是聚合签名和聚合公钥相关的函数

getMyPrivkey(phrase, pd_passphrase)

说明

通过助记词和密码生成私钥

参数和返回值

Name Type Description
phrase String 助记词
pd_passphrase String 密码
Return String 私钥

返回错误

  • Construct Secret Key

getMyPubkey(private)

说明

通过私钥生成公钥

参数和返回值

Name Type Description
private String 私钥
Return String 公钥

返回错误

  • Null KeyPair Pointer
  • Normal Error

getMyAddress**(pubkey, network)**

说明

生成地址

参数和返回值

Name Type Description
pubkey String 公钥
network String 比特币网络类型,支持“mainnet”,“signet”, “testnet”, “regtest”
Return String 地址

返回错误

  • Invalid Public Bytes

getRound1State()

说明

Musig2生成第一轮的状态.

参数和返回值

Name Type Description
Return OpaquePointer? 第一轮状态

返回错误

  • null pointer

getRound1Msg(state)

说明

通过第一轮状态生成消息,用于传递给其他参与者

参数和返回值

Name Type Description
state OpaquePointer? 第一轮状态
Return String 第一轮消息

返回错误

  • Null Round1 State Pointer
  • Normal Error

encodeRound1State(state)

说明

对第一轮状态序列化

参数和返回值

Name Type Description
state OpaquePointer? 第一轮状态
Return String 序列化结果

返回错误

  • Null Round1 State Pointer
  • Encode Fail

decodeRound1State(round1_state)

说明

对第一轮状态反序列化

参数和返回值

Name Type Description
round1_state String encodeRound1State的输出值
Return OpaquePointer? 第一轮状态

返回错误

  • null pointer

getRound2Msg(state, msg, priv, pubkeys, received_round1_msg)

说明

生成第二轮消息

参数和返回值

Name Type Description
state OpaquePointer? encodeRound1State的输出值
msg String 待签名的消息,通常是getSighash的返回值
priv String 当前参与者私钥
pubkeys [String] 所有多签参与者公钥
received_round1_msg [String] 接收到的其他多签参与者的第一轮消息
Return String 第二轮消息

返回错误

  • Invalid Round2 Msg

getAggSignature(round2_msg)

说明

返回聚合签名的结果

参数和返回值

Name Type Description
round2_msg String 所有参与者的第二轮消息
Return String 签名结果

返回错误

  • Normal Error
  • Null Round2 State Pointer

getAggPublicKey(pubkeys)

说明

生成聚合公钥

参数和返回值

Name Type Description
pubkeys [String] 待聚合的公钥列表
Return String 聚合公钥

返回错误

  • Normal Error

getUnsignedTx(tx)

说明

generateRawTx生成的未签名的交易原文,携带有自定义的附加信息,不是有效的交易原文。getUnsignedTx的目的是生成有效的未签名的交易原文,能被BTC网络解析。

参数和返回值

Name Type Description
tx String 携带附加信息的未签名的交易原文
Return String 生成有效的未签名的交易原文

返回错误

  • Invalid Transaction

Mast

下面是生成门限地址和proof相关的函数

generateThresholdPubkey(pubkeys, threshold, proto)

说明

生成门限公钥

参数和返回值

Name Type Description
pubkeys [String] 所有的公钥列表
threshold UInt8 阈值
proto String 协议名称,btc:"", brc20: "brc20", runes:"runes"
Return String 聚合公钥

返回错误

  • Invalid Public Bytes

generateControlBlock(pubkeys, threshold, aggPubkey, proto)

说明

生成proof

参数和返回值

Name Type Description
pubkeys [String] 所有的公钥列表
threshold UInt8 阈值
aggPubkey String 本次多签参与者的聚合公钥
proto String 协议名称,btc:"", brc20: "brc20", runes:"runes"
Return String proof

返回错误

  • Invalid Public Bytes

Example

下面示例提供了:构造非门限地址,非门限地址的花费,构造门限签名地址,门限签名地址花费。完整代码可以在ViewController.swift中查看。

Details

生成非门限签名地址

  1. 传入助记词和密码,生成私钥

    let private0 = getMyPrivkey(phrase: PHRASE0, pd_passphrase: "")
  2. 生成公钥

    let pubkey0 = getMyPubkey(priv: private0)
  3. 生成地址

    let addr0 = getMyAddress(pubkey: pubkey0, network: "signet");

非门限签名地址的花费

  1. 通过generateRawTx创建一笔未签名的交易。txids和indexs用于构造交易的所有输入,一个txid和一个index用来定位唯一一笔未花费的输出。下面prev_txs,txids和input_indexs长度一致并且一一对应。addresses和amounts用于构造交易的所有输出,一个adddress和一个amount表示向一个地址发送多少币。adddress没有顺序要求,只需amounts一一对应即可。与这里1f8e0f7dfa37b184244d022cdf2bc7b8e0bac8b52143ea786fa3f7bbe049eeae1唯一确定了一笔未花费的输出,这个未花费的输出所属的地址是一个非门限地址。用txid可以查询到相应的p rev_tx。35516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38代表着op_return,它所对应的amout为0。tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw是接收方的地址,100000是转账金额。tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68是找零地址,400000是找零金额。计算方式参考手续费和找零余额计算

    var prev_txs = ["020000000001014be640313b023c3c731b7e89c3f97bebcebf9772ea2f7747e5604f4483a447b601000000000000000002a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bbc027090000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01404dc68b31efc1468f84db7e9716a84c19bbc53c2d252fd1d72fa6469e860a74486b0990332b69718dbcb5acad9d48634d23ee9c215ab15fb16f4732bed1770fdf00000000"];
    var txids: [String] = ["1f8e0f7dfa37b184244d022cdf2bc7b8e0bac8b52143ea786fa3f7bbe049eeae"];
    var input_indexs: [UInt32] = [1];
    var addresses: [String]  = ["tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw", "35516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38", "tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68"];
    var amounts: [UInt64] = [100000, 0, 400000];
    var base_tx = generateRawTx(prev_txs: prev_txs, txids: txids, input_indexs:input_indexs, addresses:addresses, amounts: amounts);
    var final_tx = base_tx;
  2. 对要花费的输出进行签名。对要花费的UTXO进行签名首先要计算出这笔未花费输出的sighash,签名是对sighash进行签名.

    txid以及input_index用来定位那笔要花费的输出,agg_pubkey对于非门限签名地址填空字符串"",sigversion对于非门限签名地址填0,tx是当前构造的交易。注意计算sighash的时候,永远要用上面generateRawTx构造出的结果不能改变。

    let sighash = getSighash(tx: base_tx, txid: txids[i],input_index: input_indexs[i], agg_pubkey: "", sigversion: 0, proto: "");

    计算完sighash后,再使用私钥对其进行签名。message就是指sighash,privkey就是私钥。

    let schnorr_signature = generateSchnorrSignature(message: sighash, privkey: private_key);
  3. 将上面的签名组装进交易。tx就是当前要构造的交易,txid和input_index仍然用来定位tx中签名对应的输入。

    final_tx = buildTaprootTx(tx: final_tx, signature: schnorr_signature, txid: txids[i], input_index: input_indexs[i]);

    注意如果tx中有多个输入,那么需要重复Step2和Step3对每个输出进行签名并添加到tx中,如下图所示的for循环:。

生成门限签名地址

  1. 如下生成一个2-of-3的门限签名地址,。首先传入所有参与者的公钥和阈值即可生成门限公钥。

    let threshold_pubkey = generateThresholdPubkey(pubkeys: [pubkey0, pubkey1, pubkey2], threshold: 2, proto:"");
  2. 再将公钥编码成地址,就可以得到门限地址

    let threshold_address = getMyAddress(pubkey: threshold_pubkey, network: "signet");

门限签名地址的花费

  1. 通过generateRawTx创建一笔未签名的交易。txids和indexs用于构造交易的所有输入,一个txid和一个index用来定位唯一一笔未花费的输出。下面prev_txs,txids和input_indexs长度一致并且一一对应。addresses和amounts用于构造交易的所有输出,一个adddress和一个amount表示向一个地址发送多少币。adddress没有顺序要求,只需amounts一一对应即可。这里8e5d37c768acc4f3e794a10ad27bf0256237c80c22fa67117e3e3e1aec22ea5f0唯一确定了一笔未花费的输出,注意这个未花费的输出所属的地址是一个门限地址。用txid可以查询到相应的p rev_tx。tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68是接收方的地址,50000是转账金额。tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw是找零地址,40000是找零金额。当然这里也可以带op_return。计算方式参考手续费和找零余额计算

    prev_txs = [ "02000000000101aeee49e0bbf7a36f78ea4321b5c8bae0b8c72bdf2c024d2484b137fa7d0f8e1f01000000000000000003a0860100000000002251209a9ea267884f5549c206b2aec2bd56d98730f90532ea7f7154d4d4f923b7e3bb0000000000000000326a3035516a706f3772516e7751657479736167477a6334526a376f737758534c6d4d7141754332416255364c464646476a38801a060000000000225120c9929543dfa1e0bb84891acd47bfa6546b05e26b7a04af8eb6765fcc969d565f01409e325889515ed47099fdd7098e6fafdc880b21456d3f368457de923f4229286e34cef68816348a0581ae5885ede248a35ac4b09da61a7b9b90f34c200872d2e300000000"];
    txids = ["8e5d37c768acc4f3e794a10ad27bf0256237c80c22fa67117e3e3e1aec22ea5f"];
    input_indexs = [0];
    addresses = ["tb1pexff2s7l58sthpyfrtx500ax234stcnt0gz2lr4kwe0ue95a2e0srxsc68", "tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw"];
    amounts = [50000, 40000];
    base_tx = generateRawTx(prev_txs: prev_txs, txids: txids, input_indexs: input_indexs, addresses:addresses, amounts: amounts);
    final_tx = base_tx
  2. 对要花费的输出进行签名。对要花费的UTXO进行签名首先要计算出这笔未花费输出的sighash,签名是对sighash进行签名。

    txid以及input_index用来定位那笔要花费的输出,agg_pubkey对于门限签名地址填空字符串聚合公钥,如下是B和C两个人进行聚签花费,那么就填入B和C的聚合公钥。sigversion对于门限签名地址填1,tx是当前构造的交易。注意计算sighash的时候,永远要用上面generateRawTx构造出的结果不能改变。

    计算sighash

    let pubkey_bc = getAggPublicKey(pubkeys: [pubkey_b, pubkey_c])
    let sighash = getSighash(tx: base_tx, txid: txids[i], input_index: input_indexs[i], agg_pubkey: pubkey_bc, sigversion: 1, proto: "");

    计算签名:计算完sighash后,B和C两个人利用Musig2进行聚合签名。签名的消息就是sighash。

    var round1_state0 = getRound1State()
    let state_str = encodeRound1State(state: round1_state0);
    round1_state0 = decodeRound1State(round1_state: state_str)
    let round1_state1 = getRound1State()
    let round1_msg0 = getRound1Msg(state: round1_state0)
    let round1_msg1 = getRound1Msg(state: round1_state1)
    let round2_msg0 = getRound2Msg(state: round1_state0, msg: sighash, priv: private_b, pubkeys: [pubkey_b, pubkey_c], received_round1_msg:[round1_msg1])
    let round2_msg1 = getRound2Msg(state: round1_state1, msg: sighash, priv: private_c, pubkeys: [pubkey_b, pubkey_c], received_round1_msg:[round1_msg0])
    let multi_signature = getAggSignature(round2_msg: [round2_msg0, round2_msg1])

    ​ 下面是对上述Musig2多签的过程的详细介绍,分为如下几步:

    1. 生成第一轮的状态

      var round1_state0 = getRound1State()
    2. 通过第一轮状态获取第一轮消息,并传递给其他签名参与者。

      let round1_msg0 = getRound1Msg(state: round1_state0)
    3. 拿到其他签名参与者的第一轮消息,生成第二轮消息,并传递给其他参与者。received_round1_msg是接收到的其他参与者的第一轮消息。pubkeys是所有参与者的公钥。msg是待签名的消息。state是第一轮的状态。priv是签名者私钥。

      let round2_msg0 = getRound2Msg(state: round1_state0, msg: sighash, priv: private_b, pubkeys: [pubkey_b, pubkey_c], received_round1_msg:[round1_msg1])
    4. 利用所有参与者的第二轮消息,生成聚合签名。round2_msg 是所有参与者的第二轮消息。

      let multi_signature = getAggSignature(round2_msg: [round2_msg0, round2_msg1])

    计算proof: 门限签名的花费不仅需要签名,还要计算proof。需要传入所有人的公钥,阈值和本次签名参与者B和C的聚合公钥。

    let control_block = generateControlBlock(pubkeys: [pubkey_a, pubkey_b, pubkey_c], threshold: 2, agg_pubkey: pubkey_bc, proto: "")
  3. 将上面的签名和proof组装进行交易。tx就是当前要构造的交易,agg_signature是B和C的聚合签名,agg_pubkey是B和C的聚合公钥,txid和input_index仍然用来定位tx中签名对应的输入,txid和input_index对应的未花费输出与第二步是对应的。

    final_tx = buildThresholdTx(tx: final_tx, agg_signature: multi_signature, agg_pubkey: pubkey_bc, control: control_block, txid: txids[i], input_index: input_indexs[i], proto: "");

    注意如果tx中有多个输入,那么需要重复Step2和Step3对每个输出进行签名并添加到tx中,如下图所示的for循环:

手续费和找零余额计算

背景: A要转账给B 2BTC, C 3BTC

  1. 通过A的地址找到所有未花费的交易txids和余额,并从大到小排序,假设为[(txid1, 4), (txid2, 2), (tixd3, 1), (tixd4, 1)]

  2. 对txids和余额列表累加并找到大于输出金额2+3=5的txid,也就是txid2,未找到则返回不允许转账。

  3. 从txid2向后顺延一位,用[(txid1, 4), (txid2, 2), (tixd3, 1)]作为输入。如果txid2是最后一个,用[(txid1, 4), (txid2, 2)]作为输入。

  4. 利用输入和输出的个数以及如下公式,估计交易字节数:

    非门限地址花费的字节数估计

    105 + 58 * input_count(threshold_address) + 43 * output_count
    

    input_count(taproot_address)表示非门限地址花费时输入txid的个数

    门限地址的字节数估计

    105 + 141 * input_count(threshold_address) + 43 * output_count
    

    input_count(threshold_address)表示门限地址花费时输入txid的个数

  5. 利用字节数乘以当前FEE RATES得到交易手续费。

  6. 输入总金额 - (输出总金额+手续费)得到找零金额。 如果为负则没有找零(即输出列表不填入找零地址和金额),此时交易手续费成了输入总金额 - 输出总金额

Other

  • 将地址转成比特币交易中输出的锁定脚本script_pubkey

    let script_pubkey = getScriptPubkey(addr: "tb1pn202yeugfa25nssxk2hv902kmxrnp7g9xt487u256n20jgahuwasdcjfdw")
  • 传入一组交易以及对应的一组索引用来定位一组要花费的输出

    let spend_outputs = generateSpentOutputs(prev_txs: prev_txs, input_indexs: input_indexs)
  • generateRawTx生成的未签名的交易原文中提取有效的未签名的交易原文。

    print("unsigned tx:", getUnsignedTx(tx:base_tx))