diff --git a/bscript/script.go b/bscript/script.go index 1df416e9..9a740cb7 100644 --- a/bscript/script.go +++ b/bscript/script.go @@ -26,6 +26,8 @@ const ( ScriptTypePubKey = "pubkey" ScriptTypePubKeyHash = "pubkeyhash" ScriptTypeNonStandard = "nonstandard" + ScriptTypeEmpty = "empty" + ScriptTypeSecureHash = "securehash" ScriptTypeMultiSig = "multisig" ScriptTypeNullData = "nulldata" ) @@ -349,7 +351,7 @@ func (s *Script) PublicKeyHash() ([]byte, error) { // ScriptType returns the type of script this is as a string. func (s *Script) ScriptType() string { if len(*s) == 0 { - return ScriptTypeNonStandard + return ScriptTypeEmpty } if s.IsP2PKH() { return ScriptTypePubKeyHash diff --git a/examples/create_tx/create_tx.go b/examples/create_tx/create_tx.go index b58c7864..df105540 100644 --- a/examples/create_tx/create_tx.go +++ b/examples/create_tx/create_tx.go @@ -22,7 +22,7 @@ func main() { decodedWif, _ := wif.DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") - if err := tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: decodedWif.PrivKey}); err != nil { + if err := tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: decodedWif.PrivKey}); err != nil { log.Fatal(err.Error()) } log.Printf("tx: %s\n", tx) diff --git a/examples/create_tx_with_opreturn/create_tx_with_opreturn.go b/examples/create_tx_with_opreturn/create_tx_with_opreturn.go index 1d31c75a..b023eaa3 100644 --- a/examples/create_tx_with_opreturn/create_tx_with_opreturn.go +++ b/examples/create_tx_with_opreturn/create_tx_with_opreturn.go @@ -24,7 +24,7 @@ func main() { decodedWif, _ := wif.DecodeWIF("L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu") - err := tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: decodedWif.PrivKey}) + err := tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: decodedWif.PrivKey}) if err != nil { log.Fatal(err.Error()) } diff --git a/localsigner.go b/localsigner.go deleted file mode 100644 index 9ea8b8ec..00000000 --- a/localsigner.go +++ /dev/null @@ -1,56 +0,0 @@ -package bt - -import ( - "context" - - "github.com/libsv/go-bk/bec" - "github.com/libsv/go-bt/v2/bscript" - "github.com/libsv/go-bt/v2/sighash" -) - -// LocalSigner implements the Signer interface. It is used to sign Tx Inputs locally -// using a bkec PrivateKey. -type LocalSigner struct { - PrivateKey *bec.PrivateKey -} - -// LocalSignerGetter implements the SignerGetter interface. It is used to sign Tx Inputs locally -// using a bkec PrivateKey. -type LocalSignerGetter struct { - PrivateKey *bec.PrivateKey -} - -// Signer builds a new *bt.LocalSigner with the same private key as the calling *bt.LocalSignerGetter -func (lsc *LocalSignerGetter) Signer(ctx context.Context, lockingScript *bscript.Script) (Signer, error) { - return &LocalSigner{PrivateKey: lsc.PrivateKey}, nil -} - -// Sign a transaction at a given input index using the PrivateKey passed in through the -// InternalSigner struct. -func (is *LocalSigner) Sign(ctx context.Context, unsignedTx *Tx, index uint32, - shf sighash.Flag) (publicKey []byte, signature []byte, err error) { - - if shf == 0 { - shf = sighash.AllForkID - } - - var sh []byte - if sh, err = unsignedTx.CalcInputSignatureHash(index, shf); err != nil { - return - } - - return is.SignHash(ctx, sh) -} - -// SignHash a transaction at a given a hash digest using the PrivateKey passed in through the -// InternalSigner struct. -func (is *LocalSigner) SignHash(ctx context.Context, hash []byte) (publicKey, signature []byte, err error) { - sig, err := is.PrivateKey.Sign(hash) - if err != nil { - return - } - - publicKey = is.PrivateKey.PubKey().SerialiseCompressed() - signature = sig.Serialise() - return -} diff --git a/localunlocker.go b/localunlocker.go new file mode 100644 index 00000000..b10b90fb --- /dev/null +++ b/localunlocker.go @@ -0,0 +1,60 @@ +package bt + +import ( + "context" + "errors" + + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" +) + +// LocalUnlockerGetter implements the UnlockerGetter interface. It unlocks a Tx locally, +// using a bec PrivateKey. +type LocalUnlockerGetter struct { + PrivateKey *bec.PrivateKey +} + +// Unlocker builds a new *bt.LocalUnlocker with the same private key +// as the calling *bt.LocalUnlockerGetter. +func (lg *LocalUnlockerGetter) Unlocker(ctx context.Context, lockingScript *bscript.Script) (Unlocker, error) { + return &LocalUnlocker{PrivateKey: lg.PrivateKey}, nil +} + +// LocalUnlocker implements the unlocker interface. It is used to unlock a tx locally using a +// bec Private Key. +type LocalUnlocker struct { + PrivateKey *bec.PrivateKey +} + +// Unlock a transaction at a given input using the PrivateKey passed in through the LocalUnlocker +// struct. +// Unlock generates, applies, and returns an ECDSA signature for the provided hash digest using the private key +// as well as the public key corresponding to the private key used. The produced +// signature is deterministic (same message and same key yield the same signature) and +// canonical in accordance with RFC6979 and BIP0062. +func (lu *LocalUnlocker) Unlock(ctx context.Context, tx *Tx, idx uint32, shf sighash.Flag) error { + if shf == 0 { + shf = sighash.AllForkID + } + + sh, err := tx.CalcInputSignatureHash(idx, shf) + if err != nil { + return err + } + + sig, err := lu.PrivateKey.Sign(sh) + if err != nil { + return err + } + + pubKey := lu.PrivateKey.PubKey().SerialiseCompressed() + signature := sig.Serialise() + + switch tx.Inputs[idx].PreviousTxScript.ScriptType() { + case bscript.ScriptTypePubKeyHash: + return tx.ApplyP2PKHUnlockingScript(idx, pubKey, signature, shf) + } + + return errors.New("currently only p2pkh supported") +} diff --git a/localsigner_test.go b/localunlocker_test.go similarity index 53% rename from localsigner_test.go rename to localunlocker_test.go index 27b20f27..c13674a3 100644 --- a/localsigner_test.go +++ b/localunlocker_test.go @@ -2,20 +2,21 @@ package bt_test import ( "context" - "encoding/hex" "testing" - . "github.com/libsv/go-bk/wif" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/wif" "github.com/libsv/go-bt/v2" "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" "github.com/stretchr/testify/assert" ) -func TestInternalSigner_SignAll(t *testing.T) { +func TestLocalUnlocker_UnlockAll(t *testing.T) { t.Parallel() - unsignedTx := "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d25072326510000000000ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000" - tx, err := bt.NewTxFromString(unsignedTx) + incompleteTx := "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d25072326510000000000ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000" + tx, err := bt.NewTxFromString(incompleteTx) assert.NoError(t, err) assert.NotNil(t, tx) @@ -25,114 +26,193 @@ func TestInternalSigner_SignAll(t *testing.T) { assert.NoError(t, err) // Our private key - var wif *WIF - wif, err = DecodeWIF("cNGwGSc7KRrTmdLUZ54fiSXWbhLNDc2Eg5zNucgQxyQCzuQ5YRDq") + var w *wif.WIF + w, err = wif.DecodeWIF("cNGwGSc7KRrTmdLUZ54fiSXWbhLNDc2Eg5zNucgQxyQCzuQ5YRDq") assert.NoError(t, err) - signer := bt.LocalSignerGetter{PrivateKey: wif.PrivKey} - err = tx.SignAll(context.Background(), &signer) + unlocker := bt.LocalUnlockerGetter{PrivateKey: w.PrivKey} + err = tx.UnlockAll(context.Background(), &unlocker) assert.NoError(t, err) expectedSignedTx := "010000000193a35408b6068499e0d5abd799d3e827d9bfe70c9b75ebe209c91d2507232651000000006b483045022100c1d77036dc6cd1f3fa1214b0688391ab7f7a16cd31ea4e5a1f7a415ef167df820220751aced6d24649fa235132f1e6969e163b9400f80043a72879237dab4a1190ad412103b8b40a84123121d260f5c109bc5a46ec819c2e4002e5ba08638783bfb4e01435ffffffff02404b4c00000000001976a91404ff367be719efa79d76e4416ffb072cd53b208888acde94a905000000001976a91404d03f746652cfcb6cb55119ab473a045137d26588ac00000000" - assert.Equal(t, expectedSignedTx, hex.EncodeToString(tx.Bytes())) + assert.Equal(t, expectedSignedTx, tx.String()) + assert.NotEqual(t, incompleteTx, tx.String()) +} + +func TestLocalUnlocker_ValidSignature(t *testing.T) { + tests := map[string]struct { + tx *bt.Tx + }{ + "valid signature 1": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From("45be95d2f2c64e99518ffbbce03fb15a7758f20ee5eecf0df07938d977add71d", 0, "76a914c7c6987b6e2345a6b138e3384141520a0fbc18c588ac", 15564838601)) + + script1, err := bscript.NewFromHexString("76a91442f9682260509ac80722b1963aec8a896593d16688ac") + assert.NoError(t, err) + + assert.NoError(t, tx.AddP2PKHOutputFromScript(script1, 375041432)) + + script2, err := bscript.NewFromHexString("76a914c36538e91213a8100dcb2aed456ade363de8483f88ac") + assert.NoError(t, err) + + assert.NoError(t, tx.AddP2PKHOutputFromScript(script2, 15189796941)) + + return tx + }(), + }, + "valid signature 2": { + tx: func() *bt.Tx { + tx := bt.NewTx() + + assert.NoError( + t, + tx.From("64faeaa2e3cbadaf82d8fa8c7ded508cb043c5d101671f43c084be2ac6163148", 1, "76a914343cadc47d08a14ef773d70b3b2a90870b67b3ad88ac", 5000000000), + ) + tx.Inputs[0].SequenceNumber = 0xfffffffe + + script1, err := bscript.NewFromHexString("76a9140108b364bbbddb222e2d0fac1ad4f6f86b10317688ac") + assert.NoError(t, err) + + assert.NoError(t, tx.AddP2PKHOutputFromScript(script1, 2200000000)) + + script2, err := bscript.NewFromHexString("76a9143ac52294c730e7a4e9671abe3e7093d8834126ed88ac") + assert.NoError(t, err) + + assert.NoError(t, tx.AddP2PKHOutputFromScript(script2, 2799998870)) + return tx + }(), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tx := test.tx - // TODO: what is this for? - // if unsignedTx == expectedSignedTx { - // t.Errorf("Expected and signed TX strings in code identical") - // } + var w *wif.WIF + w, err := wif.DecodeWIF("cNGwGSc7KRrTmdLUZ54fiSXWbhLNDc2Eg5zNucgQxyQCzuQ5YRDq") + assert.NoError(t, err) + + unlocker := &bt.LocalUnlocker{PrivateKey: w.PrivKey} + assert.NoError(t, unlocker.Unlock(context.Background(), tx, 0, sighash.AllForkID)) + + parts, err := bscript.DecodeParts(*tx.Inputs[0].UnlockingScript) + assert.NoError(t, err) + + sigBytes := parts[0] + publicKeyBytes := parts[1] + + publicKey, err := bec.ParsePubKey(publicKeyBytes, bec.S256()) + assert.NoError(t, err) + + sig, err := bec.ParseDERSignature(sigBytes, bec.S256()) + assert.NoError(t, err) + + sh, err := tx.CalcInputSignatureHash(0, sighash.AllForkID) + assert.NoError(t, err) + + assert.True(t, sig.Verify(sh, publicKey)) + }) + } +} + +type mockUnlockerGetter struct { + t *testing.T + unlockerFunc func(ctx context.Context, lockingScript *bscript.Script) (bt.Unlocker, error) +} + +func (m *mockUnlockerGetter) Unlocker(ctx context.Context, lockingScript *bscript.Script) (bt.Unlocker, error) { + assert.NotNil(m.t, m.unlockerFunc, "unlockerFunc not set in this test") + return m.unlockerFunc(ctx, lockingScript) +} + +type mockUnlocker struct { + t *testing.T + script string +} + +func (m *mockUnlocker) Unlock(ctx context.Context, tx *bt.Tx, idx uint32, shf sighash.Flag) error { + script, err := bscript.NewFromASM(m.script) + assert.NoError(m.t, err) + + return tx.ApplyUnlockingScript(idx, script) +} + +func TestLocalUnlocker_NonSignature(t *testing.T) { + t.Parallel() + tests := map[string]struct { + tx *bt.Tx + unlockerFunc func(ctx context.Context, lockingScript *bscript.Script) (bt.Unlocker, error) + expUnlockingScripts []string + }{ + "simple script": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From("45be95d2f2c64e99518ffbbce03fb15a7758f20ee5eecf0df07938d977add71d", 0, "52529387", 15564838601)) + return tx + }(), + unlockerFunc: func(ctx context.Context, lockingScript *bscript.Script) (bt.Unlocker, error) { + asm, err := lockingScript.ToASM() + assert.NoError(t, err) + + unlocker, ok := map[string]*mockUnlocker{ + "OP_2 OP_2 OP_ADD OP_EQUAL": {t: t, script: "OP_4"}, + }[asm] + + assert.True(t, ok) + assert.NotNil(t, unlocker) + + return unlocker, nil + }, + expUnlockingScripts: []string{"OP_4"}, + }, + "multiple inputs unlocked": { + tx: func() *bt.Tx { + tx := bt.NewTx() + assert.NoError(t, tx.From("45be95d2f2c64e99518ffbbce03fb15a7758f20ee5eecf0df07938d977add71d", 0, "52529487", 15564838601)) + assert.NoError(t, tx.From("45be95d2f2c64e99518ffbbce03fb15a7758f20ee5eecf0df07938d977add71d", 0, "52589587", 15564838601)) + assert.NoError(t, tx.From("45be95d2f2c64e99518ffbbce03fb15a7758f20ee5eecf0df07938d977add71d", 0, "5a559687", 15564838601)) + return tx + }(), + unlockerFunc: func(ctx context.Context, lockingScript *bscript.Script) (bt.Unlocker, error) { + asm, err := lockingScript.ToASM() + assert.NoError(t, err) + + unlocker, ok := map[string]*mockUnlocker{ + "OP_2 OP_2 OP_SUB OP_EQUAL": {t: t, script: "OP_FALSE"}, + "OP_2 OP_8 OP_MUL OP_EQUAL": {t: t, script: "OP_16"}, + "OP_10 OP_5 OP_DIV OP_EQUAL": {t: t, script: "OP_2"}, + }[asm] + + assert.True(t, ok) + assert.NotNil(t, unlocker) + + return unlocker, nil + }, + expUnlockingScripts: []string{"OP_FALSE", "OP_16", "OP_2"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + tx := test.tx + assert.Equal(t, len(tx.Inputs), len(test.expUnlockingScripts)) + + ug := &mockUnlockerGetter{ + t: t, + unlockerFunc: test.unlockerFunc, + } + assert.NoError(t, tx.UnlockAll(context.Background(), ug)) + for i, script := range test.expUnlockingScripts { + asm, err := tx.Inputs[i].UnlockingScript.ToASM() + assert.NoError(t, err) + + assert.Equal(t, script, asm) + } + }) + } } -// TODO: fix/ update to use internal signer -// func TestValidSignature(t *testing.T) { -// txHex := "02000000011dd7ad77d93879f00dcfeee50ef258775ab13fe0bcfb8f51994ec6f2d295be45000000006a47304402204dbf87fe0bbf435170eea32ed9fa573cf41214b9a7146ca4101eed5738d03e3b02204d86617d7c2bba34874e4a00d3471ff5846d504ece7c67ae0623e2ca516fd0fd412103f4563d1b75b914dfba48fec433b35f56307504ec9fdaa568725619bbae26adf8ffffffff0298ad5a16000000001976a91442f9682260509ac80722b1963aec8a896593d16688ac4de86189030000001976a914c36538e91213a8100dcb2aed456ade363de8483f88ac00000000" -// tx, err := transaction.NewTxFromString(txHex) -// if err != nil { -// t.Error(err) -// return -// } -// -// // txid := tx.GetTxID() -// // fmt.Println(txid) -// -// sigScript := tx.Inputs[0].UnlockingScript -// -// publicKeyBytes := []byte(*sigScript)[len(*sigScript)-33:] -// sigBytes := []byte(*sigScript)[1 : len(*sigScript)-35] -// sigHashType, _ := binary.Uvarint([]byte(*sigScript)[len(*sigScript)-35 : len(*sigScript)-34]) -// -// publicKey, err := bec.ParsePubKey(publicKeyBytes, bec.S256()) -// if err != nil { -// t.Error(err) -// return -// } -// sig, err := bec.ParseDERSignature(sigBytes, bec.S256()) -// if err != nil { -// t.Error(err) -// return -// } -// -// var previousTxSatoshis uint64 = 15564838601 -// -// var previousTxScript, _ = bscript.NewFromHexString("76a914c7c6987b6e2345a6b138e3384141520a0fbc18c588ac") -// var prevIndex, outIndex uint32 -// -// sighash := signature.GetSighashForInputValidation(tx, uint32(sigHashType), outIndex, prevIndex, previousTxSatoshis, previousTxScript) -// -// h, err := hex.DecodeString(sighash) -// if err != nil { -// t.Error(err) -// return -// } -// valid := sig.Verify(ReverseBytes(h), publicKey) -// t.Logf("%v\n", valid) -// -// } -// -// func TestValidSignature2(t *testing.T) { -// txHex := "0200000001483116c62abe84c0431f6701d1c543b08c50ed7d8cfad882afadcbe3a2eafa64010000006a4730440220665740bdf8cf402f0a3cfeb9a7b82645132190e3c3bd605e0811b79c9dd675e002207929a958673cebe60a6af9fa1fa89e7f3fc397727df5798500d58906c3886a44412103401136395f6c679c6176cdf499ff54720acfb56c07028feaafdce68d79463a45feffffff0200562183000000001976a9140108b364bbbddb222e2d0fac1ad4f6f86b10317688ac9697e4a6000000001976a9143ac52294c730e7a4e9671abe3e7093d8834126ed88ac6f640800" -// tx, err := transaction.NewTxFromString(txHex) -// if err != nil { -// t.Error(err) -// return -// } -// -// // txid := tx.GetTxID() -// // fmt.Println(txid) -// -// sigScript := tx.Inputs[0].UnlockingScript -// -// publicKeyBytes := []byte(*sigScript)[len(*sigScript)-33:] -// sigBytes := []byte(*sigScript)[1 : len(*sigScript)-35] -// sigHashType, _ := binary.Uvarint([]byte(*sigScript)[len(*sigScript)-35 : len(*sigScript)-34]) -// -// publicKey, err := bec.ParsePubKey(publicKeyBytes, bec.S256()) -// if err != nil { -// t.Error(err) -// return -// } -// sig, err := bec.ParseDERSignature(sigBytes, bec.S256()) -// if err != nil { -// t.Error(err) -// return -// } -// -// var previousTxSatoshis uint64 = 5000000000 -// -// var previousTxScript, _ = bscript.NewFromHexString("76a914343cadc47d08a14ef773d70b3b2a90870b67b3ad88ac") -// var prevIndex uint32 = 1 -// var outIndex uint32 -// -// sighash := signature.GetSighashForInputValidation(tx, uint32(sigHashType), outIndex, prevIndex, previousTxSatoshis, previousTxScript) -// -// h, err := hex.DecodeString(sighash) -// if err != nil { -// t.Error(err) -// return -// } -// valid := sig.Verify(ReverseBytes(h), publicKey) -// t.Logf("%v\n", valid) -// -// } // // func TestBareMultiSigValidation(t *testing.T) { // txHex := "0100000001cfb38c76cadeb5b96c3863d9e298fe96e24e594b75f69c37aa709f45b76d1b25000000009200483045022100d83dc84d3ea3fb36b006f6887e1e16811c59fe9a9b79b84142874a90d5b834160220052967be98c26270de0082b0fecab5a40d5bc48d5034b6cdfc2b8e47210e1469414730440220099ffa89363f9a05f23a4fa318ddbefeeeec4b41f6abde7083a3be6696ed904902201722110a488df3780a260ba09b7de6363bfce7f6beec9819e9b9f47f6e978d8141ffffffff01a8840100000000001976a91432b996f742e774b0241be9007f831558ba06d20b88ac00000000" diff --git a/signaturehash.go b/signaturehash.go index 49d32725..f6649a0e 100644 --- a/signaturehash.go +++ b/signaturehash.go @@ -195,17 +195,15 @@ func (tx *Tx) CalcInputPreimageLegacy(inputNumber uint32, shf sighash.Flag) ([]b } } - switch shf & sighash.Mask { // nolint:exhaustive // no need - case sighash.None: + if shf.HasWithMask(sighash.None) { txCopy.Outputs = txCopy.Outputs[0:0] for i := range txCopy.Inputs { if i != int(inputNumber) { txCopy.Inputs[i].SequenceNumber = 0 } } - case sighash.Single: + } else if shf.HasWithMask(sighash.Single) { txCopy.Outputs = txCopy.Outputs[:inputNumber+1] - for i := 0; i < int(inputNumber); i++ { txCopy.Outputs[i].Satoshis = 18446744073709551615 // -1 but underflowed txCopy.Outputs[i].LockingScript = &bscript.Script{} @@ -216,8 +214,6 @@ func (tx *Tx) CalcInputPreimageLegacy(inputNumber uint32, shf sighash.Flag) ([]b txCopy.Inputs[i].SequenceNumber = 0 } } - case sighash.Old, sighash.All: - default: } if shf&sighash.AnyOneCanPay != 0 { diff --git a/signer.go b/signer.go deleted file mode 100644 index 087bd2ba..00000000 --- a/signer.go +++ /dev/null @@ -1,24 +0,0 @@ -package bt - -import ( - "context" - - "github.com/libsv/go-bt/v2/bscript" - "github.com/libsv/go-bt/v2/sighash" -) - -// Signer interface to allow custom implementations of different signing mechanisms. -// Implement the Sign function as shown in InternalSigner, for example. Sign generates -// and returns an ECDSA signature for the provided hash digest using the private key -// as well as the public key corresponding to the private key used. The produced -// signature is deterministic (same message and same key yield the same signature) and -// canonical in accordance with RFC6979 and BIP0062. -type Signer interface { - Sign(ctx context.Context, unsignedTx *Tx, index uint32, shf sighash.Flag) (publicKey, signature []byte, err error) - SignHash(ctx context.Context, hash []byte) (publicKey, signature []byte, err error) -} - -// SignerGetter interfaces getting a signer for a given output/locking script. -type SignerGetter interface { - Signer(ctx context.Context, lockingScript *bscript.Script) (Signer, error) -} diff --git a/tx.go b/tx.go index e185b5d1..fac61369 100644 --- a/tx.go +++ b/tx.go @@ -46,10 +46,10 @@ var ( // DO NOT CHANGE ORDER - Optimised memory via malign // type Tx struct { - Inputs []*Input `json:"inputs"` - Outputs []*Output `json:"outputs"` - Version uint32 `json:"version"` - LockTime uint32 `json:"lockTime"` + Inputs []*Input + Outputs []*Output + Version uint32 + LockTime uint32 } // NewTx creates a new transaction object with default values. diff --git a/tx_test.go b/tx_test.go index 69734cc7..9f147079 100644 --- a/tx_test.go +++ b/tx_test.go @@ -212,7 +212,7 @@ func TestTx_CreateTx(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) } @@ -246,7 +246,7 @@ func TestTx_HasDataOutputs(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, true, tx.HasDataOutputs()) @@ -272,7 +272,7 @@ func TestTx_HasDataOutputs(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, false, tx.HasDataOutputs()) @@ -643,7 +643,7 @@ func Test_EstimateIsFeePaidEnough(t *testing.T) { assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559)) assert.NoError(t, tx.ChangeToAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", bt.NewFeeQuote())) - tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) return tx }(), expSize: &bt.TxSize{ @@ -664,7 +664,7 @@ func Test_EstimateIsFeePaidEnough(t *testing.T) { )) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 904)) - tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) return tx }(), expSize: &bt.TxSize{ @@ -685,7 +685,7 @@ func Test_EstimateIsFeePaidEnough(t *testing.T) { )) assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello"))) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 894)) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.Nil(t, err) return tx }(), @@ -708,7 +708,7 @@ func Test_EstimateIsFeePaidEnough(t *testing.T) { )) assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello"))) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 895)) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.Nil(t, err) return tx }(), @@ -828,7 +828,7 @@ func Test_IsFeePaidEnough(t *testing.T) { assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 256559)) assert.NoError(t, tx.ChangeToAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", bt.NewFeeQuote())) - tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) return tx }(), expSize: &bt.TxSize{ @@ -847,7 +847,7 @@ func Test_IsFeePaidEnough(t *testing.T) { 0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000)) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 904)) - tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) return tx }(), expSize: &bt.TxSize{ @@ -866,7 +866,7 @@ func Test_IsFeePaidEnough(t *testing.T) { 0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000)) assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello"))) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 894)) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.Nil(t, err) return tx }(), @@ -887,7 +887,7 @@ func Test_IsFeePaidEnough(t *testing.T) { 0, "76a914ff8c9344d4e76c0580420142f697e5fc2ce5c98e88ac", 1000)) assert.NoError(t, tx.AddOpReturnOutput([]byte("hellohello"))) assert.NoError(t, tx.AddP2PKHOutputFromAddress("mtestD3vRB7AoYWK2n6kLdZmAMLbLhDsLr", 895)) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.Nil(t, err) return tx }(), diff --git a/txchange_test.go b/txchange_test.go index 57ca2580..7f060fbb 100644 --- a/txchange_test.go +++ b/txchange_test.go @@ -83,7 +83,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, expectedTx.String(), tx.String()) @@ -109,7 +109,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) // Correct fee for the tx @@ -151,7 +151,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, @@ -191,7 +191,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, "01000000010b94a1ef0fb352aa2adc54207ce47ba55d5a1c1609afda58fe9520e472299107000000006a473044022049ee0c0f26c00e6a6b3af5990fc8296c66eab3e3e42ab075069b89b1be6fefec02206079e49dd8c9e1117ef06fbe99714d822620b1f0f5d19f32a1128f5d29b7c3c4412102c8803fdd437d902f08e3c2344cb33065c99d7c99982018ff9f7219c3dd352ff0ffffffff01a0083d00000000001976a914af2590a45ae401651fdbdf59a76ad43d1862534088ac00000000", tx.String()) @@ -224,7 +224,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, "01000000010b94a1ef0fb352aa2adc54207ce47ba55d5a1c1609afda58fe9520e472299107000000006a47304402206bbb4b23349bdf86e6fbc9067226e9a7b15c977fa530999b39cd0a6d9c83360d02202dd8ffdc610e58b3fc92b44400d99e38c78866765f31acb40d98007a52e7a826412102c8803fdd437d902f08e3c2344cb33065c99d7c99982018ff9f7219c3dd352ff0ffffffff0240420f00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88acc0c62d00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88ac00000000", tx.String()) @@ -258,7 +258,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, "01000000010b94a1ef0fb352aa2adc54207ce47ba55d5a1c1609afda58fe9520e472299107000000006b483045022100fd07316603e9abf393e695192e8ce1e7f808d2735cc57039109a2210ad32d9a7022000e301e2a988b23ab3872b041df8b6eb0315238e0918944cbaf8b6abdde75cac412102c8803fdd437d902f08e3c2344cb33065c99d7c99982018ff9f7219c3dd352ff0ffffffff023b420f00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88acc0c62d00000000001976a914b6aa34534d2b11e66b438c7525f819aee01e397c88ac00000000", tx.String()) @@ -295,7 +295,7 @@ func TestTx_Change(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, wif) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.Equal(t, "01000000028ee20a442cdbcc9f9f927d9c2c9370e611675ebc24c064e8e94508ec8eca889e000000006b483045022100f88298f5a380244dd5b91f70be99394f8e562d2a61976ca8cf2aaeb381ee6e6a0220069243fc951061b624cf96124263b857a65a53400846080b543e4a8c16e097ce4121034aaeabc056f33fd960d1e43fc8a0672723af02f275e54c31381af66a334634caffffffff42eaf7bdddc797a0beb97717ff8846f03c963fb5fe15a2b555b9cbd477b0254e000000006b483045022100afa7a986e6e0faf725a9779fe8e61fd19b5973544dc7707fd758cdd45912332a0220760fe07fc8610d867be5281f29778e3cd1a18a6eef74470d0f1a4ede95c848924121034aaeabc056f33fd960d1e43fc8a0672723af02f275e54c31381af66a334634caffffffff01c82b0000000000001976a9147824dec00be2c45dad83c9b5e9f5d7ef05ba3cf988ac00000000", tx.String()) diff --git a/txjson_node_test.go b/txjson_node_test.go index 78beab08..13883f78 100644 --- a/txjson_node_test.go +++ b/txjson_node_test.go @@ -31,7 +31,7 @@ func TestTxJSON_Node_JSON(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, w) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), @@ -54,7 +54,7 @@ func TestTxJSON_Node_JSON(t *testing.T) { tx.AddOutput(&bt.Output{ LockingScript: s, }) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), @@ -151,7 +151,7 @@ func TestTxJSON_Node_MarshallJSON(t *testing.T) { w, err := wif.DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") assert.NoError(t, err) assert.NotNil(t, w) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), diff --git a/txjson_test.go b/txjson_test.go index 9f6666b2..a9d6a401 100644 --- a/txjson_test.go +++ b/txjson_test.go @@ -31,7 +31,7 @@ func TestTx_JSON(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, w) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), @@ -54,7 +54,7 @@ func TestTx_JSON(t *testing.T) { tx.AddOutput(&bt.Output{ LockingScript: s, }) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), @@ -135,7 +135,7 @@ func TestTx_MarshallJSON(t *testing.T) { w, err := wif.DecodeWIF("KznvCNc6Yf4iztSThoMH6oHWzH9EgjfodKxmeuUGPq5DEX5maspS") assert.NoError(t, err) assert.NotNil(t, w) - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: w.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: w.PrivKey}) assert.NoError(t, err) return tx }(), diff --git a/txsign.go b/txsign.go deleted file mode 100644 index fb75f298..00000000 --- a/txsign.go +++ /dev/null @@ -1,105 +0,0 @@ -package bt - -import ( - "context" - "errors" - "fmt" - - "github.com/libsv/go-bt/v2/bscript" - "github.com/libsv/go-bt/v2/sighash" -) - -type signerFunc func(context.Context, Signer, uint32, sighash.Flag) error - -// Sign is used to sign the transaction at a specific input index. -// It takes a Signed interface as a parameter so that different -// signing implementations can be used to sign the transaction - -// for example internal/local or external signing (hardware wallet). -func (tx *Tx) Sign(ctx context.Context, s Signer, index uint32, shf sighash.Flag) error { - if shf == 0 { - shf = sighash.AllForkID - } - pubKey, sig, err := s.Sign(ctx, tx, index, shf) - if err != nil { - return err - } - return tx.ApplyP2PKHUnlockingScript(index, pubKey, sig, shf) -} - -// SignHash is used to sign the transaction at a specific input index. -// It takes a Signed interface as a parameter so that different -// signing implementations can be used to sign the transaction - -// for example internal/local or external signing (hardware wallet). -// -// SignHash will only -// take the final signature hash to be signed so will need to trust that -// it is getting the right hash to sign as there no way to verify that -// it is signing the right hash. -func (tx *Tx) SignHash(ctx context.Context, s Signer, index uint32, shf sighash.Flag) error { - if shf == 0 { - shf = sighash.AllForkID - } - - sh, err := tx.CalcInputSignatureHash(index, shf) - if err != nil { - return err - } - - pubKey, sig, err := s.SignHash(ctx, sh) - if err != nil { - return err - } - - return tx.ApplyP2PKHUnlockingScript(index, pubKey, sig, shf) -} - -// ApplyP2PKHUnlockingScript applies a script to the transaction at a specific index in -// unlocking script field. -func (tx *Tx) ApplyP2PKHUnlockingScript(index uint32, pubKey []byte, sig []byte, shf sighash.Flag) error { - uls, err := bscript.NewP2PKHUnlockingScript(pubKey, sig, shf) - if err != nil { - return err - } - - return tx.ApplyUnlockingScript(index, uls) -} - -// ApplyUnlockingScript applies a script to the transaction at a specific index in -// unlocking script field. -func (tx *Tx) ApplyUnlockingScript(index uint32, s *bscript.Script) error { - if tx.Inputs[index] != nil { - tx.Inputs[index].UnlockingScript = s - return nil - } - - return fmt.Errorf("no input at index %d", index) -} - -// SignAll is used to sign all inputs. It currently only supports the signing P2PKH. -// It takes a Signed interface as a parameter so that different -// signing implementations can be used to sign the transaction - -// for example internal/local or external signing. -func (tx *Tx) SignAll(ctx context.Context, sg SignerGetter) error { - shf := sighash.AllForkID // use SIGHASHALLFORFORKID to sign automatically - // TODO: add support for other script types - signerStrats := map[string]signerFunc{ - bscript.ScriptTypePubKeyHash: tx.Sign, - } - - for i, in := range tx.Inputs { - // TODO: add support for other script types - stratFn, ok := signerStrats[in.PreviousTxScript.ScriptType()] - if !ok { - return errors.New("unsupported script type") - } - s, err := sg.Signer(ctx, in.PreviousTxScript) - if err != nil { - return err - } - if err := stratFn(ctx, s, uint32(i), shf); err != nil { - return err - } - } - - return nil -} diff --git a/txunlock.go b/txunlock.go new file mode 100644 index 00000000..24390d61 --- /dev/null +++ b/txunlock.go @@ -0,0 +1,65 @@ +package bt + +import ( + "context" + "fmt" + + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" +) + +// Unlock is used to unlock the transaction at a specific input index. +// It takes an Unlocker interface as a parameter so that different +// unlocking implementations can be used to unlock the transaction - +// for example local or external unlocking (hardware wallet), or +// signature/nonsignature based. +func (tx *Tx) Unlock(ctx context.Context, u Unlocker, idx uint32, shf sighash.Flag) error { + if shf == 0 { + shf = sighash.AllForkID + } + + return u.Unlock(ctx, tx, idx, shf) +} + +// UnlockAll is used to sign all inputs. It takes an UnlockerGetter interface +// as a parameter so that different unlocking implementations can +// be used to sign the transaction - for example local/external +// signing, or P2PKH/contract signing. +func (tx *Tx) UnlockAll(ctx context.Context, ug UnlockerGetter) error { + shf := sighash.AllForkID // use SIGHASHALLFORFORKID to sign automatically + + for i, in := range tx.Inputs { + u, err := ug.Unlocker(ctx, in.PreviousTxScript) + if err != nil { + return err + } + + if err = tx.Unlock(ctx, u, uint32(i), shf); err != nil { + return err + } + } + + return nil +} + +// ApplyP2PKHUnlockingScript applies a script to the transaction at a specific index in +// unlocking script field. +func (tx *Tx) ApplyP2PKHUnlockingScript(index uint32, pubKey []byte, sig []byte, shf sighash.Flag) error { + uls, err := bscript.NewP2PKHUnlockingScript(pubKey, sig, shf) + if err != nil { + return err + } + + return tx.ApplyUnlockingScript(index, uls) +} + +// ApplyUnlockingScript applies a script to the transaction at a specific index in +// unlocking script field. +func (tx *Tx) ApplyUnlockingScript(index uint32, s *bscript.Script) error { + if tx.Inputs[index] != nil { + tx.Inputs[index].UnlockingScript = s + return nil + } + + return fmt.Errorf("no input at index %d", index) +} diff --git a/txsign_test.go b/txunlock_test.go similarity index 83% rename from txsign_test.go rename to txunlock_test.go index 3f60cb8c..d037e0a9 100644 --- a/txsign_test.go +++ b/txunlock_test.go @@ -13,7 +13,7 @@ func TestTx_Sign(t *testing.T) { // todo: add tests } -func TestTx_SignAll(t *testing.T) { +func TestTx_UnlockAll(t *testing.T) { t.Parallel() t.Run("valid tx (basic)", func(t *testing.T) { @@ -37,7 +37,7 @@ func TestTx_SignAll(t *testing.T) { rawTxBefore := tx.String() - err = tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: wif.PrivKey}) + err = tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: wif.PrivKey}) assert.NoError(t, err) assert.NotEqual(t, rawTxBefore, tx.String()) @@ -49,7 +49,7 @@ func TestTx_SignAll(t *testing.T) { rawTxBefore := tx.String() - err := tx.SignAll(context.Background(), &bt.LocalSignerGetter{PrivateKey: nil}) + err := tx.UnlockAll(context.Background(), &bt.LocalUnlockerGetter{PrivateKey: nil}) assert.NoError(t, err) assert.Equal(t, rawTxBefore, tx.String()) diff --git a/unlocker.go b/unlocker.go new file mode 100644 index 00000000..86622f84 --- /dev/null +++ b/unlocker.go @@ -0,0 +1,19 @@ +package bt + +import ( + "context" + + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" +) + +// Unlocker interface to allow custom implementations of different unlocking mechanisms. +// Implement the Unlocker function as shown in LocalUnlocker, for example. +type Unlocker interface { + Unlock(ctx context.Context, tx *Tx, idx uint32, shf sighash.Flag) error +} + +// UnlockerGetter interfaces getting an unlocker for a given output/locking script. +type UnlockerGetter interface { + Unlocker(ctx context.Context, lockingScript *bscript.Script) (Unlocker, error) +}