From 0cb66232ef2f21a438bf9a6288156500c73e1933 Mon Sep 17 00:00:00 2001 From: Gaze Date: Fri, 22 Nov 2024 14:22:07 +0700 Subject: [PATCH] feat: add bip322 pkg --- pkg/bip322/bip322.go | 191 ++++++++++++++++++++++++++++++++++++++ pkg/bip322/bip322_test.go | 145 +++++++++++++++++++++++++++++ pkg/bip322/bip322_util.go | 77 +++++++++++++++ 3 files changed, 413 insertions(+) create mode 100644 pkg/bip322/bip322.go create mode 100644 pkg/bip322/bip322_test.go create mode 100644 pkg/bip322/bip322_util.go diff --git a/pkg/bip322/bip322.go b/pkg/bip322/bip322.go new file mode 100644 index 0000000..d6909d9 --- /dev/null +++ b/pkg/bip322/bip322.go @@ -0,0 +1,191 @@ +package bip322 + +// This package is forked from https://github.com/unisat-wallet/libbrc20-indexer/blob/v1.1.0/utils/bip322/verify.go, +// with a few modifications to make the interface more friendly with Gaze types. + +import ( + "crypto/sha256" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/pkg/btcutils" +) + +func GetSha256(data []byte) (hash []byte) { + sha := sha256.New() + sha.Write(data[:]) + hash = sha.Sum(nil) + return +} + +func GetTagSha256(data []byte) (hash []byte) { + tag := []byte("BIP0322-signed-message") + hashTag := GetSha256(tag) + var msg []byte + msg = append(msg, hashTag...) + msg = append(msg, hashTag...) + msg = append(msg, data...) + return GetSha256(msg) +} + +func PrepareTx(pkScript []byte, message string) (toSign *wire.MsgTx, err error) { + // Create a new transaction to spend + toSpend := wire.NewMsgTx(0) + + // Decode the message hash + messageHash := GetTagSha256([]byte(message)) + + // Create the script for to_spend + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(messageHash) + scriptSig, err := builder.Script() + if err != nil { + return nil, errors.WithStack(err) + } + + // Create a TxIn with the outpoint 000...000:FFFFFFFF + prevOutHash, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000000") + prevOut := wire.NewOutPoint(prevOutHash, wire.MaxPrevOutIndex) + txIn := wire.NewTxIn(prevOut, scriptSig, nil) + txIn.Sequence = 0 + + toSpend.AddTxIn(txIn) + toSpend.AddTxOut(wire.NewTxOut(0, pkScript)) + + // Create a transaction for to_sign + toSign = wire.NewMsgTx(0) + hash := toSpend.TxHash() + + prevOutSpend := wire.NewOutPoint((*chainhash.Hash)(hash.CloneBytes()), 0) + + txSignIn := wire.NewTxIn(prevOutSpend, nil, nil) + txSignIn.Sequence = 0 + toSign.AddTxIn(txSignIn) + + // Create the script for to_sign + builderPk := txscript.NewScriptBuilder() + builderPk.AddOp(txscript.OP_RETURN) + scriptPk, err := builderPk.Script() + if err != nil { + return nil, errors.WithStack(err) + } + toSign.AddTxOut(wire.NewTxOut(0, scriptPk)) + return toSign, nil +} + +func VerifyMessage(address *btcutils.Address, signature []byte, message string) bool { + if len(signature) == 0 { + // empty signature is invalid + return false + } + + // BIP322 signature format is the serialized witness of the toSign transaction. + // [0x02] [SIGNATURE_LEN, ...(signature that go into witness[0])] [PUBLIC_KEY_LEN, ...(public key that was used to sign the message, go to witness[1])] + witness, err := DeserializeWitnessSignature(signature) + if err != nil { + // invalid signature + return false + } + + return verifySignatureWitness(witness, address.ScriptPubKey(), message) +} + +// verifySignatureWitness +// signature: 64B, pkScript: 33B, message: any +func verifySignatureWitness(witness wire.TxWitness, pkScript []byte, message string) bool { + toSign, err := PrepareTx(pkScript, message) + if err != nil { + return false + } + + toSign.TxIn[0].Witness = witness + prevFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, 0, + ) + hashCache := txscript.NewTxSigHashes(toSign, prevFetcher) + vm, err := txscript.NewEngine(pkScript, toSign, 0, txscript.StandardVerifyFlags, nil, hashCache, 0, prevFetcher) + if err != nil { + return false + } + if err := vm.Execute(); err != nil { + return false + } + return true +} + +func SignMessage(privateKey *btcec.PrivateKey, address *btcutils.Address, message string) ([]byte, error) { + var witness wire.TxWitness + var err error + switch address.Type() { + case btcutils.AddressP2TR: + witness, _, err = SignSignatureTaproot(privateKey, message) + case btcutils.AddressP2WPKH: + witness, _, err = SignSignatureP2WPKH(privateKey, message) + } + if err != nil { + return nil, errors.WithStack(err) + } + signature, err := SerializeWitnessSignature(witness) + if err != nil { + return nil, errors.WithStack(err) + } + return signature, nil +} + +func SignSignatureTaproot(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) { + pubKey := txscript.ComputeTaprootKeyNoScript(privKey.PubKey()) + + pkScript, err = PayToTaprootScript(pubKey) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + toSign, err := PrepareTx(pkScript, message) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + prevFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, 0, + ) + sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher) + + witness, err = txscript.TaprootWitnessSignature( + toSign, sigHashes, 0, 0, pkScript, + txscript.SigHashDefault, privKey, + ) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return witness, pkScript, nil +} + +func SignSignatureP2WPKH(privKey *btcec.PrivateKey, message string) (witness wire.TxWitness, pkScript []byte, err error) { + pubKey := privKey.PubKey() + pkScript, err = PayToWitnessScript(pubKey) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + toSign, err := PrepareTx(pkScript, message) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + prevFetcher := txscript.NewCannedPrevOutputFetcher( + pkScript, 0, + ) + sigHashes := txscript.NewTxSigHashes(toSign, prevFetcher) + + witness, err = txscript.WitnessSignature(toSign, sigHashes, + 0, 0, pkScript, txscript.SigHashAll, + privKey, true) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return witness, pkScript, nil +} diff --git a/pkg/bip322/bip322_test.go b/pkg/bip322/bip322_test.go new file mode 100644 index 0000000..ac4f385 --- /dev/null +++ b/pkg/bip322/bip322_test.go @@ -0,0 +1,145 @@ +package bip322 + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/gaze-network/indexer-network/pkg/btcutils" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVerifyMessage(t *testing.T) { + type testcase struct { + Address string + Message string + Signature string // base64 + Expected bool + } + testcases := []testcase{ + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + Expected: true, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy", + Expected: true, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + Expected: true, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy", + Expected: true, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVA", + Expected: false, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX", + Expected: false, + }, + { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "BkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDLXXXX", + Expected: false, + }, + { + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "", + Signature: "AUDVvVp7mCtPZtoORKYcMM+idx9yy5+z4TGeoI/PWEUscd5x0QYJ6IPQ/anBSMWPWSRPqHVrEjOIWhP9FsZSMFdG", + Expected: true, + }, + { + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "", + Signature: "AUDYeG/k6AL9pNuhgK8aJqxIqBIObX867yc3QgdfS70sWEdUg0Msv0Ps24Pt5aQmcI2wZdwI3Egp5tA5PW+wTOw6", + Expected: true, + }, + { + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World", + Signature: "AUCkOlzIYSN6T+QzENjlp61Pa2l4EyDDH8c4pFANOwoh3oGi/iZHscAExUSePhbS94KIMgcg+yNp+LsckO+AfLQQ", + Expected: true, + }, + { + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World", + Signature: "AUD5MwxtURP3tAip3fS5vVRwa4L15wEyTIG0BQ3DPktJpXvQe7Sh8kf+mVaO4ldEP+vhiVZ/sXvOHEbQQnsiYpCq", + Expected: true, + }, + } + for _, tc := range testcases { + t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) { + address, err := btcutils.SafeNewAddress(tc.Address) + require.NoError(t, err) + signature, err := base64.StdEncoding.DecodeString(tc.Signature) + require.NoError(t, err) + + verified := VerifyMessage(&address, signature, tc.Message) + assert.Equal(t, tc.Expected, verified) + }) + } +} + +func TestSignMessage(t *testing.T) { + type testcase struct { + PrivateKey *btcec.PrivateKey + Address string + Message string + } + + testcases := []testcase{ + { + PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey, + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + }, + { + PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey, + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + }, + { + PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey, + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "", + }, + { + PrivateKey: lo.Must(btcutil.DecodeWIF("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k")).PrivKey, + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World", + }, + } + + for _, tc := range testcases { + t.Run(fmt.Sprintf("%s_%s", tc.Address, tc.Message), func(t *testing.T) { + address, err := btcutils.SafeNewAddress(tc.Address) + require.NoError(t, err) + signature, err := SignMessage(tc.PrivateKey, &address, tc.Message) + require.NoError(t, err) + + verified := VerifyMessage(&address, signature, tc.Message) + assert.True(t, verified) + }) + } +} diff --git a/pkg/bip322/bip322_util.go b/pkg/bip322/bip322_util.go new file mode 100644 index 0000000..d22a931 --- /dev/null +++ b/pkg/bip322/bip322_util.go @@ -0,0 +1,77 @@ +package bip322 + +import ( + "bytes" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/cockroachdb/errors" + "github.com/gaze-network/indexer-network/common/errs" +) + +func SerializeWitnessSignature(witness wire.TxWitness) ([]byte, error) { + result := new(bytes.Buffer) + buf := make([]byte, 8) + + if err := wire.WriteVarIntBuf(result, 0, uint64(len(witness)), buf); err != nil { + return nil, errors.WithStack(err) + } + for _, item := range witness { + if err := wire.WriteVarBytesBuf(result, 0, item, buf); err != nil { + return nil, errors.WithStack(err) + } + } + return result.Bytes(), nil +} + +func DeserializeWitnessSignature(serialized []byte) (wire.TxWitness, error) { + if len(serialized) == 0 { + return nil, errors.Wrap(errs.ArgumentRequired, "serialized witness is required") + } + witness := make(wire.TxWitness, 0) + + current := 0 + witnessLen := int(serialized[current]) + current++ + for i := 0; i < witnessLen; i++ { + if current >= len(serialized) { + return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes") + } + witnessItemLen := int(serialized[current]) + current++ + if current+witnessItemLen > len(serialized) { + return nil, errors.Wrap(errs.InvalidArgument, "invalid serialized witness data: not enough bytes") + } + witnessItem := serialized[current : current+witnessItemLen] + current += witnessItemLen + witness = append(witness, witnessItem) + } + return witness, nil +} + +// PayToTaprootScript creates a pk script for a pay-to-taproot output key. +func PayToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) { + script, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_1). + AddData(schnorr.SerializePubKey(taprootKey)). + Script() + if err != nil { + return nil, errors.WithStack(err) + } + return script, nil +} + +// PayToWitnessScript creates a pk script for a pay-to-wpkh output key. +func PayToWitnessScript(pubkey *btcec.PublicKey) ([]byte, error) { + script, err := txscript.NewScriptBuilder(). + AddOp(txscript.OP_0). + AddData(btcutil.Hash160(pubkey.SerializeCompressed())). + Script() + if err != nil { + return nil, errors.WithStack(err) + } + return script, nil +}