From 9305aabe1dcecc5a05f1a772c0ca32a09e22d39c Mon Sep 17 00:00:00 2001 From: oxf71 <144757737+oxf71@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:21:31 +0800 Subject: [PATCH] update --- example/inscribewithoutnoderpc/main.go | 109 +++++ go.mod | 2 + go.sum | 4 + lib/go-ord-tx/pkg/ord/ord.go | 569 +++++++++++++++++++++++++ lib/go-ord-tx/pkg/ord/sign.go | 107 +++++ 5 files changed, 791 insertions(+) create mode 100644 example/inscribewithoutnoderpc/main.go create mode 100644 lib/go-ord-tx/pkg/ord/ord.go create mode 100644 lib/go-ord-tx/pkg/ord/sign.go diff --git a/example/inscribewithoutnoderpc/main.go b/example/inscribewithoutnoderpc/main.go new file mode 100644 index 0000000..cb86e61 --- /dev/null +++ b/example/inscribewithoutnoderpc/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "encoding/hex" + "fmt" + "log" + + "github.com/oxf71/musig2-demo/lib/go-ord-tx/pkg/btcapi/mempool" + "github.com/oxf71/musig2-demo/lib/go-ord-tx/pkg/ord" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +func main() { + netParams := &chaincfg.SigNetParams + btcApiClient := mempool.NewClient(netParams) + + utxoPrivateKeyHex := "440bb3ec56d213e90d006d344d74f6478db4f7fa4cdd388095d8f4edef0c5156" + utxoPrivateKeyBytes, err := hex.DecodeString(utxoPrivateKeyHex) + if err != nil { + log.Fatal(err) + } + utxoPrivateKey, _ := btcec.PrivKeyFromBytes(utxoPrivateKeyBytes) + + utxoPublicKey := utxoPrivateKey.PubKey() + + utxoTaprootAddress, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(utxoPublicKey)), netParams) + if err != nil { + log.Fatal(err) + } + + fmt.Println("utxoTaprootAddress:", utxoTaprootAddress.EncodeAddress()) + + unspentList, err := btcApiClient.ListUnspent(utxoTaprootAddress) + + if err != nil { + log.Fatalf("list unspent err %v", err) + } + + if len(unspentList) == 0 { + log.Fatal("no unspent") + } + + commitTxOutPointList := make([]*wire.OutPoint, 0) + commitTxPrivateKeyList := make([]*btcec.PrivateKey, 0) + for i := range unspentList { + commitTxOutPointList = append(commitTxOutPointList, unspentList[i].Outpoint) + commitTxPrivateKeyList = append(commitTxPrivateKeyList, utxoPrivateKey) + fmt.Println("unspentList:", unspentList[i].Outpoint) + } + + // panic("err") + + dataList := make([]ord.InscriptionData, 0) + + dataList = append(dataList, ord.InscriptionData{ + ContentType: "text/plain;charset=utf-8", + Body: []byte("Create Without full Node "), + Destination: "tb1p3m6qfu0mzkxsmaue0hwekrxm2nxfjjrmv4dvy94gxs8c3s7zns6qcgf8ef", + }) + + request := ord.InscriptionRequest{ + CommitTxOutPointList: commitTxOutPointList, + CommitTxPrivateKeyList: commitTxPrivateKeyList, + CommitFeeRate: 18, + FeeRate: 19, + DataList: dataList, + SingleRevealTxOnly: false, + } + + tool, err := ord.NewInscriptionToolWithBtcApiClient(netParams, btcApiClient, &request) + if err != nil { + log.Fatalf("Failed to create inscription tool: %v", err) + } + recoveryKeyWIFList := tool.GetRecoveryKeyWIFList() + for i, recoveryKeyWIF := range recoveryKeyWIFList { + log.Printf("recoveryKeyWIF %d %s \n", i, recoveryKeyWIF) + } + + commitTxHex, err := tool.GetCommitTxHex() + if err != nil { + log.Fatalf("get commit tx hex err, %v", err) + } + log.Printf("commitTxHex %s \n", commitTxHex) + revealTxHexList, err := tool.GetRevealTxHexList() + if err != nil { + log.Fatalf("get reveal tx hex err, %v", err) + } + for i, revealTxHex := range revealTxHexList { + log.Printf("revealTxHex %d %s \n", i, revealTxHex) + } + commitTxHash, revealTxHashList, inscriptions, fees, err := tool.Inscribe() + if err != nil { + log.Fatalf("send tx errr, %v", err) + } + log.Println("commitTxHash, " + commitTxHash.String()) + for i := range revealTxHashList { + log.Println("revealTxHash, " + revealTxHashList[i].String()) + } + for i := range inscriptions { + log.Println("inscription, " + inscriptions[i]) + } + log.Println("fees: ", fees) +} diff --git a/go.mod b/go.mod index 5243bd5..3fd1ea8 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,13 @@ require ( ) require ( + github.com/aead/siphash v1.0.1 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed // indirect ) diff --git a/go.sum b/go.sum index ae88254..8d6d852 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= @@ -41,6 +42,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -51,6 +53,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -71,6 +74,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/lib/go-ord-tx/pkg/ord/ord.go b/lib/go-ord-tx/pkg/ord/ord.go new file mode 100644 index 0000000..5102f06 --- /dev/null +++ b/lib/go-ord-tx/pkg/ord/ord.go @@ -0,0 +1,569 @@ +package ord + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "log" + + "github.com/oxf71/musig2-demo/lib/go-ord-tx/pkg/btcapi" + extRpcClient "github.com/oxf71/musig2-demo/lib/go-ord-tx/pkg/rpcclient" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/mempool" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/pkg/errors" +) + +type InscriptionData struct { + ContentType string + Body []byte + Destination string +} + +type InscriptionRequest struct { + CommitTxOutPointList []*wire.OutPoint + CommitTxPrivateKeyList []*btcec.PrivateKey // If used without RPC, + // a local signature is required for committing the commit tx. + // Currently, CommitTxPrivateKeyList[i] sign CommitTxOutPointList[i] + CommitFeeRate int64 + FeeRate int64 + DataList []InscriptionData + SingleRevealTxOnly bool // Currently, the official Ordinal parser can only parse a single NFT per transaction. + // When the official Ordinal parser supports parsing multiple NFTs in the future, we can consider using a single reveal transaction. + RevealOutValue int64 +} + +type inscriptionTxCtxData struct { + privateKey *btcec.PrivateKey + inscriptionScript []byte + commitTxAddressPkScript []byte + controlBlockWitness []byte + recoveryPrivateKeyWIF string + revealTxPrevOutput *wire.TxOut +} + +type blockchainClient struct { + rpcClient *rpcclient.Client + btcApiClient btcapi.BTCAPIClient +} + +type InscriptionTool struct { + net *chaincfg.Params + client *blockchainClient + commitTxPrevOutputFetcher *txscript.MultiPrevOutFetcher + commitTxPrivateKeyList []*btcec.PrivateKey + txCtxDataList []*inscriptionTxCtxData + revealTxPrevOutputFetcher *txscript.MultiPrevOutFetcher + revealTx []*wire.MsgTx + commitTx *wire.MsgTx +} + +const ( + defaultSequenceNum = wire.MaxTxInSequenceNum - 10 + defaultRevealOutValue = int64(500) // 500 sat, ord default 10000 + + MaxStandardTxWeight = blockchain.MaxBlockWeight / 10 +) + +func NewInscriptionTool(net *chaincfg.Params, rpcclient *rpcclient.Client, request *InscriptionRequest) (*InscriptionTool, error) { + tool := &InscriptionTool{ + net: net, + client: &blockchainClient{ + rpcClient: rpcclient, + }, + commitTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), + txCtxDataList: make([]*inscriptionTxCtxData, len(request.DataList)), + revealTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), + } + return tool, tool._initTool(net, request) +} + +func NewInscriptionToolWithBtcApiClient(net *chaincfg.Params, btcApiClient btcapi.BTCAPIClient, request *InscriptionRequest) (*InscriptionTool, error) { + if len(request.CommitTxPrivateKeyList) != len(request.CommitTxOutPointList) { + return nil, errors.New("the length of CommitTxPrivateKeyList and CommitTxOutPointList should be the same") + } + tool := &InscriptionTool{ + net: net, + client: &blockchainClient{ + btcApiClient: btcApiClient, + }, + commitTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), + commitTxPrivateKeyList: request.CommitTxPrivateKeyList, + revealTxPrevOutputFetcher: txscript.NewMultiPrevOutFetcher(nil), + } + return tool, tool._initTool(net, request) +} + +func (tool *InscriptionTool) _initTool(net *chaincfg.Params, request *InscriptionRequest) error { + revealOutValue := defaultRevealOutValue + if request.RevealOutValue > 0 { + revealOutValue = request.RevealOutValue + } + tool.txCtxDataList = make([]*inscriptionTxCtxData, len(request.DataList)) + destinations := make([]string, len(request.DataList)) + for i := 0; i < len(request.DataList); i++ { + txCtxData, err := createInscriptionTxCtxData(net, request.DataList[i]) + if err != nil { + return err + } + tool.txCtxDataList[i] = txCtxData + destinations[i] = request.DataList[i].Destination + } + totalRevealPrevOutput, err := tool.buildEmptyRevealTx(request.SingleRevealTxOnly, destinations, revealOutValue, request.FeeRate) + if err != nil { + return err + } + err = tool.buildCommitTx(request.CommitTxOutPointList, totalRevealPrevOutput, request.CommitFeeRate) + if err != nil { + return err + } + err = tool.completeRevealTx() + if err != nil { + return err + } + err = tool.signCommitTx() + if err != nil { + return errors.Wrap(err, "sign commit tx error") + } + return err +} + +func createInscriptionTxCtxData(net *chaincfg.Params, data InscriptionData) (*inscriptionTxCtxData, error) { + privateKey, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + inscriptionBuilder := txscript.NewScriptBuilder(). + AddData(schnorr.SerializePubKey(privateKey.PubKey())). + AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_FALSE). + AddOp(txscript.OP_IF). + AddData([]byte("ord")). + // Two OP_DATA_1 should be OP_1. However, in the following link, it's not set as OP_1: + // https://github.com/casey/ord/blob/0.5.1/src/inscription.rs#L17 + // Therefore, we use two OP_DATA_1 to maintain consistency with ord. + AddOp(txscript.OP_DATA_1). + AddOp(txscript.OP_DATA_1). + AddData([]byte(data.ContentType)). + AddOp(txscript.OP_0) + maxChunkSize := 520 + bodySize := len(data.Body) + for i := 0; i < bodySize; i += maxChunkSize { + end := i + maxChunkSize + if end > bodySize { + end = bodySize + } + // to skip txscript.MaxScriptSize 10000 + inscriptionBuilder.AddFullData(data.Body[i:end]) + } + inscriptionScript, err := inscriptionBuilder.Script() + if err != nil { + return nil, err + } + // to skip txscript.MaxScriptSize 10000 + inscriptionScript = append(inscriptionScript, txscript.OP_ENDIF) + + leafNode := txscript.NewBaseTapLeaf(inscriptionScript) + proof := &txscript.TapscriptProof{ + TapLeaf: leafNode, + RootNode: leafNode, + } + + controlBlock := proof.ToControlBlock(privateKey.PubKey()) + controlBlockWitness, err := controlBlock.ToBytes() + if err != nil { + return nil, err + } + + tapHash := proof.RootNode.TapHash() + commitTxAddress, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootOutputKey(privateKey.PubKey(), tapHash[:])), net) + if err != nil { + return nil, err + } + commitTxAddressPkScript, err := txscript.PayToAddrScript(commitTxAddress) + if err != nil { + return nil, err + } + + recoveryPrivateKeyWIF, err := btcutil.NewWIF(txscript.TweakTaprootPrivKey(*privateKey, tapHash[:]), net, true) + if err != nil { + return nil, err + } + + return &inscriptionTxCtxData{ + privateKey: privateKey, + inscriptionScript: inscriptionScript, + commitTxAddressPkScript: commitTxAddressPkScript, + controlBlockWitness: controlBlockWitness, + recoveryPrivateKeyWIF: recoveryPrivateKeyWIF.String(), + }, nil +} + +func (tool *InscriptionTool) buildEmptyRevealTx(singleRevealTxOnly bool, destination []string, revealOutValue, feeRate int64) (int64, error) { + var revealTx []*wire.MsgTx + totalPrevOutput := int64(0) + total := len(tool.txCtxDataList) + addTxInTxOutIntoRevealTx := func(tx *wire.MsgTx, index int) error { + in := wire.NewTxIn(&wire.OutPoint{Index: uint32(index)}, nil, nil) + in.Sequence = defaultSequenceNum + tx.AddTxIn(in) + receiver, err := btcutil.DecodeAddress(destination[index], tool.net) + if err != nil { + return err + } + scriptPubKey, err := txscript.PayToAddrScript(receiver) + if err != nil { + return err + } + out := wire.NewTxOut(revealOutValue, scriptPubKey) + tx.AddTxOut(out) + return nil + } + if singleRevealTxOnly { + revealTx = make([]*wire.MsgTx, 1) + tx := wire.NewMsgTx(wire.TxVersion) + for i := 0; i < total; i++ { + err := addTxInTxOutIntoRevealTx(tx, i) + if err != nil { + return 0, err + } + } + eachRevealBaseTxFee := int64(tx.SerializeSize()) * feeRate / int64(total) + prevOutput := (revealOutValue + eachRevealBaseTxFee) * int64(total) + { + emptySignature := make([]byte, 64) + emptyControlBlockWitness := make([]byte, 33) + for i := 0; i < total; i++ { + fee := (int64(wire.TxWitness{emptySignature, tool.txCtxDataList[i].inscriptionScript, emptyControlBlockWitness}.SerializeSize()+2+3) / 4) * feeRate + tool.txCtxDataList[i].revealTxPrevOutput = &wire.TxOut{ + PkScript: tool.txCtxDataList[i].commitTxAddressPkScript, + Value: revealOutValue + eachRevealBaseTxFee + fee, + } + prevOutput += fee + } + } + totalPrevOutput = prevOutput + revealTx[0] = tx + } else { + revealTx = make([]*wire.MsgTx, total) + for i := 0; i < total; i++ { + tx := wire.NewMsgTx(wire.TxVersion) + err := addTxInTxOutIntoRevealTx(tx, i) + if err != nil { + return 0, err + } + prevOutput := revealOutValue + int64(tx.SerializeSize())*feeRate + { + emptySignature := make([]byte, 64) + emptyControlBlockWitness := make([]byte, 33) + fee := (int64(wire.TxWitness{emptySignature, tool.txCtxDataList[i].inscriptionScript, emptyControlBlockWitness}.SerializeSize()+2+3) / 4) * feeRate + prevOutput += fee + tool.txCtxDataList[i].revealTxPrevOutput = &wire.TxOut{ + PkScript: tool.txCtxDataList[i].commitTxAddressPkScript, + Value: prevOutput, + } + } + totalPrevOutput += prevOutput + revealTx[i] = tx + } + } + tool.revealTx = revealTx + return totalPrevOutput, nil +} + +func (tool *InscriptionTool) getTxOutByOutPoint(outPoint *wire.OutPoint) (*wire.TxOut, error) { + var txOut *wire.TxOut + if tool.client.rpcClient != nil { + tx, err := tool.client.rpcClient.GetRawTransactionVerbose(&outPoint.Hash) + if err != nil { + return nil, err + } + if int(outPoint.Index) >= len(tx.Vout) { + return nil, errors.New("err out point") + } + vout := tx.Vout[outPoint.Index] + pkScript, err := hex.DecodeString(vout.ScriptPubKey.Hex) + if err != nil { + return nil, err + } + amount, err := btcutil.NewAmount(vout.Value) + if err != nil { + return nil, err + } + txOut = wire.NewTxOut(int64(amount), pkScript) + } else { + tx, err := tool.client.btcApiClient.GetRawTransaction(&outPoint.Hash) + if err != nil { + return nil, err + } + if int(outPoint.Index) >= len(tx.TxOut) { + return nil, errors.New("err out point") + } + txOut = tx.TxOut[outPoint.Index] + } + tool.commitTxPrevOutputFetcher.AddPrevOut(*outPoint, txOut) + return txOut, nil +} + +func (tool *InscriptionTool) buildCommitTx(commitTxOutPointList []*wire.OutPoint, totalRevealPrevOutput, commitFeeRate int64) error { + totalSenderAmount := btcutil.Amount(0) + tx := wire.NewMsgTx(wire.TxVersion) + var changePkScript *[]byte + for i := range commitTxOutPointList { + txOut, err := tool.getTxOutByOutPoint(commitTxOutPointList[i]) + if err != nil { + return err + } + if changePkScript == nil { // first sender as change address + changePkScript = &txOut.PkScript + } + in := wire.NewTxIn(commitTxOutPointList[i], nil, nil) + in.Sequence = defaultSequenceNum + tx.AddTxIn(in) + + totalSenderAmount += btcutil.Amount(txOut.Value) + } + for i := range tool.txCtxDataList { + tx.AddTxOut(tool.txCtxDataList[i].revealTxPrevOutput) + } + + tx.AddTxOut(wire.NewTxOut(0, *changePkScript)) + fee := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) + changeAmount := totalSenderAmount - btcutil.Amount(totalRevealPrevOutput) - fee + if changeAmount > 0 { + tx.TxOut[len(tx.TxOut)-1].Value = int64(changeAmount) + } else { + tx.TxOut = tx.TxOut[:len(tx.TxOut)-1] + if changeAmount < 0 { + feeWithoutChange := btcutil.Amount(mempool.GetTxVirtualSize(btcutil.NewTx(tx))) * btcutil.Amount(commitFeeRate) + if totalSenderAmount-btcutil.Amount(totalRevealPrevOutput)-feeWithoutChange < 0 { + return errors.New("insufficient balance") + } + } + } + tool.commitTx = tx + return nil +} + +func (tool *InscriptionTool) completeRevealTx() error { + for i := range tool.txCtxDataList { + tool.revealTxPrevOutputFetcher.AddPrevOut(wire.OutPoint{ + Hash: tool.commitTx.TxHash(), + Index: uint32(i), + }, tool.txCtxDataList[i].revealTxPrevOutput) + if len(tool.revealTx) == 1 { + tool.revealTx[0].TxIn[i].PreviousOutPoint.Hash = tool.commitTx.TxHash() + } else { + tool.revealTx[i].TxIn[0].PreviousOutPoint.Hash = tool.commitTx.TxHash() + } + } + witnessList := make([]wire.TxWitness, len(tool.txCtxDataList)) + for i := range tool.txCtxDataList { + revealTx := tool.revealTx[0] + idx := i + if len(tool.revealTx) != 1 { + revealTx = tool.revealTx[i] + idx = 0 + } + witnessArray, err := txscript.CalcTapscriptSignaturehash(txscript.NewTxSigHashes(revealTx, tool.revealTxPrevOutputFetcher), + txscript.SigHashDefault, revealTx, idx, tool.revealTxPrevOutputFetcher, txscript.NewBaseTapLeaf(tool.txCtxDataList[i].inscriptionScript)) + if err != nil { + return err + } + signature, err := schnorr.Sign(tool.txCtxDataList[i].privateKey, witnessArray) + if err != nil { + return err + } + witnessList[i] = wire.TxWitness{signature.Serialize(), tool.txCtxDataList[i].inscriptionScript, tool.txCtxDataList[i].controlBlockWitness} + } + for i := range witnessList { + if len(tool.revealTx) == 1 { + tool.revealTx[0].TxIn[i].Witness = witnessList[i] + } else { + tool.revealTx[i].TxIn[0].Witness = witnessList[i] + } + } + // check tx max tx wight + for i, tx := range tool.revealTx { + revealWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx)) + if revealWeight > MaxStandardTxWeight { + return errors.New(fmt.Sprintf("reveal(index %d) transaction weight greater than %d (MAX_STANDARD_TX_WEIGHT): %d", i, MaxStandardTxWeight, revealWeight)) + } + } + return nil +} + +func (tool *InscriptionTool) signCommitTx() error { + if len(tool.commitTxPrivateKeyList) == 0 { + fmt.Println("by wallet sign") + commitSignTransaction, isSignComplete, err := tool.client.rpcClient.SignRawTransactionWithWallet(tool.commitTx) + if err != nil { + log.Printf("sign commit tx error, %v", err) + return err + } + if !isSignComplete { + return errors.New("sign commit tx error") + } + tool.commitTx = commitSignTransaction + } else { + fmt.Println("taproot sign") + witnessList := make([]wire.TxWitness, len(tool.commitTx.TxIn)) + for i := range tool.commitTx.TxIn { + txOut := tool.commitTxPrevOutputFetcher.FetchPrevOutput(tool.commitTx.TxIn[i].PreviousOutPoint) + witness, err := TaprootWitnessSignature( + tool.commitTx, + txscript.NewTxSigHashes(tool.commitTx, tool.commitTxPrevOutputFetcher), + i, + txOut.Value, + txOut.PkScript, + txscript.SigHashDefault, + tool.commitTxPrivateKeyList[i]) + if err != nil { + return err + } + witnessList[i] = witness + } + for i := range witnessList { + tool.commitTx.TxIn[i].Witness = witnessList[i] + } + } + return nil +} + +func (tool *InscriptionTool) BackupRecoveryKeyToRpcNode() error { + if tool.client.rpcClient == nil { + return errors.New("rpc client is nil") + } + descriptors := make([]extRpcClient.Descriptor, len(tool.txCtxDataList)) + for i := range tool.txCtxDataList { + descriptorInfo, err := tool.client.rpcClient.GetDescriptorInfo(fmt.Sprintf("rawtr(%s)", tool.txCtxDataList[i].recoveryPrivateKeyWIF)) + if err != nil { + return err + } + descriptors[i] = extRpcClient.Descriptor{ + Desc: *btcjson.String(fmt.Sprintf("rawtr(%s)#%s", tool.txCtxDataList[i].recoveryPrivateKeyWIF, descriptorInfo.Checksum)), + Timestamp: btcjson.TimestampOrNow{ + Value: "now", + }, + Active: btcjson.Bool(false), + Range: nil, + NextIndex: nil, + Internal: btcjson.Bool(false), + Label: btcjson.String("commit tx recovery key"), + } + } + results, err := extRpcClient.ImportDescriptors(tool.client.rpcClient, descriptors) + if err != nil { + return err + } + if results == nil { + return errors.New("commit tx recovery key import failed, nil result") + } + for _, result := range *results { + if !result.Success { + return errors.New("commit tx recovery key import failed") + } + } + return nil +} + +func (tool *InscriptionTool) GetRecoveryKeyWIFList() []string { + wifList := make([]string, len(tool.txCtxDataList)) + for i := range tool.txCtxDataList { + wifList[i] = tool.txCtxDataList[i].recoveryPrivateKeyWIF + } + return wifList +} + +func getTxHex(tx *wire.MsgTx) (string, error) { + var buf bytes.Buffer + if err := tx.Serialize(&buf); err != nil { + return "", err + } + return hex.EncodeToString(buf.Bytes()), nil +} + +func (tool *InscriptionTool) GetCommitTxHex() (string, error) { + return getTxHex(tool.commitTx) +} + +func (tool *InscriptionTool) GetRevealTxHexList() ([]string, error) { + txHexList := make([]string, len(tool.revealTx)) + for i := range tool.revealTx { + txHex, err := getTxHex(tool.revealTx[i]) + if err != nil { + return nil, err + } + txHexList[i] = txHex + } + return txHexList, nil +} + +func (tool *InscriptionTool) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { + if tool.client.rpcClient != nil { + return tool.client.rpcClient.SendRawTransaction(tx, false) + } else { + return tool.client.btcApiClient.BroadcastTx(tx) + } +} + +func (tool *InscriptionTool) calculateFee() int64 { + fees := int64(0) + for _, in := range tool.commitTx.TxIn { + fees += tool.commitTxPrevOutputFetcher.FetchPrevOutput(in.PreviousOutPoint).Value + } + for _, out := range tool.commitTx.TxOut { + fees -= out.Value + } + for _, tx := range tool.revealTx { + for _, in := range tx.TxIn { + fees += tool.revealTxPrevOutputFetcher.FetchPrevOutput(in.PreviousOutPoint).Value + } + for _, out := range tx.TxOut { + fees -= out.Value + } + } + return fees +} + +func (tool *InscriptionTool) Inscribe() (commitTxHash *chainhash.Hash, revealTxHashList []*chainhash.Hash, inscriptions []string, fees int64, err error) { + fees = tool.calculateFee() + + commitTx, _ := json.Marshal(tool.commitTx) + fmt.Println("commitTx json marshal:", string(commitTx)) + + commitTxHash, err = tool.sendRawTransaction(tool.commitTx) + if err != nil { + return nil, nil, nil, fees, errors.Wrap(err, "send commit tx error") + } + revealTxHashList = make([]*chainhash.Hash, len(tool.revealTx)) + inscriptions = make([]string, len(tool.txCtxDataList)) + for i := range tool.revealTx { + _revealTxHash, err := tool.sendRawTransaction(tool.revealTx[i]) + if err != nil { + return commitTxHash, revealTxHashList, nil, fees, errors.Wrap(err, fmt.Sprintf("send reveal tx error, %d。", i)) + } + revealTxHashList[i] = _revealTxHash + if len(tool.revealTx) == len(tool.txCtxDataList) { + inscriptions[i] = fmt.Sprintf("%si0", _revealTxHash) + } else { + inscriptions[i] = fmt.Sprintf("%si", _revealTxHash) + } + } + if len(tool.revealTx) != len(tool.txCtxDataList) { + for i := len(inscriptions) - 1; i > 0; i-- { + inscriptions[i] = fmt.Sprintf("%s%d", inscriptions[0], i) + } + } + return commitTxHash, revealTxHashList, inscriptions, fees, nil +} diff --git a/lib/go-ord-tx/pkg/ord/sign.go b/lib/go-ord-tx/pkg/ord/sign.go new file mode 100644 index 0000000..0f2218d --- /dev/null +++ b/lib/go-ord-tx/pkg/ord/sign.go @@ -0,0 +1,107 @@ +package ord + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +func TaprootWitnessSignature(tx *wire.MsgTx, sigHashes *txscript.TxSigHashes, idx int, + amt int64, pkScript []byte, hashType txscript.SigHashType, + key *btcec.PrivateKey) (wire.TxWitness, error) { + + // As we're assuming this was a BIP 86 key, we use an empty root hash + // which means output key commits to just the public key. + fakeTapscriptRootHash := []byte{} + + sig, err := RawTxInTaprootSignature( + tx, sigHashes, idx, amt, pkScript, fakeTapscriptRootHash, + hashType, key, + ) + if err != nil { + return nil, err + } + + // The witness script to spend a taproot input using the key-spend path + // is just the signature itself, given the public key is + // embedded in the previous output script. + return wire.TxWitness{sig}, nil +} + +func RawTxInTaprootSignature(tx *wire.MsgTx, sigHashes *txscript.TxSigHashes, idx int, + amt int64, pkScript []byte, tapScriptRootHash []byte, hashType txscript.SigHashType, + key *btcec.PrivateKey) ([]byte, error) { + + // First, we'll start by compute the top-level taproot sighash. + sigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, hashType, tx, idx, + txscript.NewCannedPrevOutputFetcher(pkScript, amt), + ) + if err != nil { + return nil, err + } + + // Before we sign the sighash, we'll need to apply the taptweak to the + // private key based on the tapScriptRootHash. + privKeyTweak := TweakTaprootPrivKey(*key, tapScriptRootHash) + + // With the sighash constructed, we can sign it with the specified + // private key. + signature, err := schnorr.Sign(privKeyTweak, sigHash) + if err != nil { + return nil, err + } + + //TODO: gen tweak + // signers := []*btcec.PrivateKey{privKeyTweak} + // taprootTweak := []byte{} + // musignature, _, _, err := musig2.MultiPartySign(signers, taprootTweak, sigHash) + // if err != nil { + // return nil, err + // } + + sig := signature.Serialize() + + // If this is sighash default, then we can just return the signature + // directly. + if hashType&txscript.SigHashDefault == txscript.SigHashDefault { + return sig, nil + } + + // Otherwise, append the sighash type to the final sig. + return append(sig, byte(hashType)), nil +} +func TweakTaprootPrivKey(privKey btcec.PrivateKey, + scriptRoot []byte) *btcec.PrivateKey { + + // If the corresponding public key has an odd y coordinate, then we'll + // negate the private key as specified in BIP 341. + privKeyScalar := privKey.Key + pubKeyBytes := privKey.PubKey().SerializeCompressed() + if pubKeyBytes[0] == secp.PubKeyFormatCompressedOdd { + privKeyScalar.Negate() + } + + // Next, we'll compute the tap tweak hash that commits to the internal + // key and the merkle script root. We'll snip off the extra parity byte + // from the compressed serialization and use that directly. + schnorrKeyBytes := pubKeyBytes[1:] + tapTweakHash := chainhash.TaggedHash( + chainhash.TagTapTweak, schnorrKeyBytes, scriptRoot, + ) + + // Map the private key to a ModNScalar which is needed to perform + // operation mod the curve order. + var tweakScalar btcec.ModNScalar + tweakScalar.SetBytes((*[32]byte)(tapTweakHash)) + + // Now that we have the private key in its may negated form, we'll add + // the script root as a tweak. As we're using a ModNScalar all + // operations are already normalized mod the curve order. + privTweak := privKeyScalar.Add(&tweakScalar) + + return btcec.PrivKeyFromScalar(privTweak) +}