Skip to content

Commit e3fc117

Browse files
authored
Merge pull request #51 from bitcoin-sv/feature/sign-unsigned
add Transaction.SignUnsigned
2 parents 118d076 + e028a95 commit e3fc117

File tree

5 files changed

+157
-28
lines changed

5 files changed

+157
-28
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8+
- [1.1.7 - 2024-09-10](#117---2024-09-10)
89
- [1.1.6 - 2024-09-09](#116---2024-09-09)
910
- [1.1.5 - 2024-09-06](#115---2024-09-06)
1011
- [1.1.4 - 2024-09-05](#114---2024-09-05)
@@ -14,6 +15,19 @@ All notable changes to this project will be documented in this file. The format
1415
- [1.1.0 - 2024-08-19](#110---2024-08-19)
1516
- [1.0.0 - 2024-06-06](#100---2024-06-06)
1617

18+
19+
## [1.1.7] - 2024-09-10
20+
- Rework `tx.Clone()` to be more efficient
21+
- Introduce SignUnsigned to sign only inputs that have not already been signed
22+
- Added tests
23+
- Other minor performance improvements.
24+
25+
### Added
26+
- New method `Transaction.SignUnsigned()`
27+
28+
### Changed
29+
- `Transaction.Clone()` does not reconstitute the source transaction from bytes. Creates a new transaction.
30+
1731
## [1.1.6] - 2024-09-09
1832
- Optimize handling of source transaction inputs. Avoid mocking up entire transaction when adding source inputs.
1933
- Minor alignment in ECIES helper function

script/interpreter/operations.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,7 +2016,7 @@ func opcodeCheckSig(op *ParsedOpcode, t *thread) error {
20162016
return err
20172017
}
20182018

2019-
txCopy := t.tx.ShallowClone()
2019+
txCopy := t.tx.Clone()
20202020
sourceTxOut := txCopy.Inputs[t.inputIdx].SourceTxOutput()
20212021
sourceTxOut.LockingScript = up
20222022

@@ -2282,7 +2282,7 @@ func opcodeCheckMultiSig(op *ParsedOpcode, t *thread) error {
22822282
}
22832283

22842284
// Generate the signature hash based on the signature hash type.
2285-
txCopy := t.tx.ShallowClone()
2285+
txCopy := t.tx.Clone()
22862286
input := txCopy.Inputs[t.inputIdx]
22872287
sourceOut := input.SourceTxOutput()
22882288
if sourceOut != nil {

transaction/signaturehash.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func (tx *Transaction) CalcInputPreimageLegacy(inputNumber uint32, shf sighash.F
188188
return defaultHex, nil
189189
}
190190

191-
txCopy := tx.ShallowClone()
191+
txCopy := tx.Clone()
192192

193193
for i := range txCopy.Inputs {
194194
if i == int(inputNumber) {

transaction/transaction.go

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import (
55
"encoding/binary"
66
"encoding/hex"
77
"io"
8-
"log"
98
"slices"
109

1110
"github.com/bitcoin-sv/go-sdk/chainhash"
1211
crypto "github.com/bitcoin-sv/go-sdk/primitives/hash"
1312
"github.com/bitcoin-sv/go-sdk/script"
1413
"github.com/bitcoin-sv/go-sdk/util"
14+
"github.com/pkg/errors"
1515
)
1616

1717
type Transaction struct {
@@ -313,26 +313,7 @@ func (tx *Transaction) BytesWithClearedInputs(index int, lockingScript []byte) [
313313
return tx.toBytesHelper(index, lockingScript, false)
314314
}
315315

316-
// Clone returns a clone of the tx
317316
func (tx *Transaction) Clone() *Transaction {
318-
// Ignore err as byte slice passed in is created from valid tx
319-
clone, err := NewTransactionFromBytes(tx.Bytes())
320-
if err != nil {
321-
log.Fatal(err)
322-
}
323-
324-
for i, input := range tx.Inputs {
325-
if input.SourceTransaction != nil {
326-
clone.Inputs[i].SourceTransaction = input.SourceTransaction.Clone()
327-
}
328-
// clone.Inputs[i].SourceTransaction = input.SourceTransaction
329-
clone.Inputs[i].sourceOutput = input.sourceOutput
330-
}
331-
332-
return clone
333-
}
334-
335-
func (tx *Transaction) ShallowClone() *Transaction {
336317
// Creating a new Tx from scratch is much faster than cloning from bytes
337318
// ~ 420ns/op vs 2200ns/op of the above function in benchmarking
338319
// this matters as we clone txs a couple of times when verifying signatures
@@ -345,9 +326,10 @@ func (tx *Transaction) ShallowClone() *Transaction {
345326

346327
for i, input := range tx.Inputs {
347328
clone.Inputs[i] = &TransactionInput{
348-
SourceTXID: (*chainhash.Hash)(input.SourceTXID[:]),
349-
SourceTxOutIndex: input.SourceTxOutIndex,
350-
SequenceNumber: input.SequenceNumber,
329+
SourceTXID: (*chainhash.Hash)(input.SourceTXID[:]),
330+
SourceTxOutIndex: input.SourceTxOutIndex,
331+
SequenceNumber: input.SequenceNumber,
332+
UnlockingScriptTemplate: input.UnlockingScriptTemplate,
351333
}
352334
if input.UnlockingScript != nil {
353335
clone.Inputs[i].UnlockingScript = input.UnlockingScript
@@ -436,7 +418,12 @@ func (tx *Transaction) AddMerkleProof(bump *MerklePath) error {
436418
return nil
437419
}
438420

421+
// Fee returns the fee of the transaction.
439422
func (tx *Transaction) Sign() error {
423+
err := tx.checkFeeComputed()
424+
if err != nil {
425+
return err
426+
}
440427
for vin, i := range tx.Inputs {
441428
if i.UnlockingScriptTemplate != nil {
442429
unlock, err := i.UnlockingScriptTemplate.Sign(tx, uint32(vin))
@@ -448,3 +435,32 @@ func (tx *Transaction) Sign() error {
448435
}
449436
return nil
450437
}
438+
439+
// SignUnsigned signs the transaction without the unlocking script.
440+
func (tx *Transaction) SignUnsigned() error {
441+
err := tx.checkFeeComputed()
442+
if err != nil {
443+
return err
444+
}
445+
for vin, i := range tx.Inputs {
446+
if i.UnlockingScript == nil {
447+
if i.UnlockingScriptTemplate != nil {
448+
unlock, err := i.UnlockingScriptTemplate.Sign(tx, uint32(vin))
449+
if err != nil {
450+
return err
451+
}
452+
i.UnlockingScript = unlock
453+
}
454+
}
455+
}
456+
return nil
457+
}
458+
459+
func (tx *Transaction) checkFeeComputed() error {
460+
for _, out := range tx.Outputs {
461+
if out.Satoshis == 0 && out.Change {
462+
return errors.New("fee not computed")
463+
}
464+
}
465+
return nil
466+
}

transaction/transaction_test.go

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package transaction_test
22

33
import (
4+
"encoding/hex"
45
"testing"
56

7+
"github.com/bitcoin-sv/go-sdk/chainhash"
68
ec "github.com/bitcoin-sv/go-sdk/primitives/ec"
79
"github.com/bitcoin-sv/go-sdk/script"
810
"github.com/bitcoin-sv/go-sdk/transaction"
@@ -57,6 +59,19 @@ func TestNewTransaction(t *testing.T) {
5759
})
5860
}
5961

62+
func TestIsCoinbase(t *testing.T) {
63+
tx, err := transaction.NewTransactionFromHex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff17033f250d2f43555656452f2c903fb60859897700d02700ffffffff01d864a012000000001976a914d648686cf603c11850f39600e37312738accca8f88ac00000000")
64+
require.NoError(t, err)
65+
require.True(t, tx.IsCoinbase())
66+
}
67+
68+
func TestIsValidTxID(t *testing.T) {
69+
valid, _ := hex.DecodeString("fe77aa03d5563d3ec98455a76655ea3b58e19a4eb102baf7b2a47af37e94b295")
70+
require.True(t, transaction.IsValidTxID(valid))
71+
invalid, _ := hex.DecodeString("fe77aa03d5563d3ec98455a76655ea3b58e19a4eb102baf7b2a47af37e94b2")
72+
require.False(t, transaction.IsValidTxID(invalid))
73+
}
74+
6075
func TestBEEF(t *testing.T) {
6176
t.Parallel()
6277
t.Run("deserialize and serialize", func(t *testing.T) {
@@ -79,13 +94,97 @@ func TestEF(t *testing.T) {
7994
})
8095
}
8196

82-
func Benchmark_ShallowClone(b *testing.B) {
97+
func TestClone(t *testing.T) {
98+
tx, err := transaction.NewTransactionFromBEEFHex(BRC62Hex)
99+
require.NoError(t, err)
100+
101+
clone := tx.Clone()
102+
require.Equal(t, tx.Bytes(), clone.Bytes())
103+
}
104+
105+
func BenchmarkClone(b *testing.B) {
83106
tx, _ := transaction.NewTransactionFromHex("0200000003a9bc457fdc6a54d99300fb137b23714d860c350a9d19ff0f571e694a419ff3a0010000006b48304502210086c83beb2b2663e4709a583d261d75be538aedcafa7766bd983e5c8db2f8b2fc02201a88b178624ab0ad1748b37c875f885930166237c88f5af78ee4e61d337f935f412103e8be830d98bb3b007a0343ee5c36daa48796ae8bb57946b1e87378ad6e8a090dfeffffff0092bb9a47e27bf64fc98f557c530c04d9ac25e2f2a8b600e92a0b1ae7c89c20010000006b483045022100f06b3db1c0a11af348401f9cebe10ae2659d6e766a9dcd9e3a04690ba10a160f02203f7fbd7dfcfc70863aface1a306fcc91bbadf6bc884c21a55ef0d32bd6b088c8412103e8be830d98bb3b007a0343ee5c36daa48796ae8bb57946b1e87378ad6e8a090dfeffffff9d0d4554fa692420a0830ca614b6c60f1bf8eaaa21afca4aa8c99fb052d9f398000000006b483045022100d920f2290548e92a6235f8b2513b7f693a64a0d3fa699f81a034f4b4608ff82f0220767d7d98025aff3c7bd5f2a66aab6a824f5990392e6489aae1e1ae3472d8dffb412103e8be830d98bb3b007a0343ee5c36daa48796ae8bb57946b1e87378ad6e8a090dfeffffff02807c814a000000001976a9143a6bf34ebfcf30e8541bbb33a7882845e5a29cb488ac76b0e60e000000001976a914bd492b67f90cb85918494767ebb23102c4f06b7088ac67000000")
84107

85108
b.Run("clone", func(b *testing.B) {
86109
for i := 0; i < b.N; i++ {
87-
clone := tx.ShallowClone()
110+
clone := tx.Clone()
88111
_ = clone
89112
}
90113
})
91114
}
115+
116+
func TestUncomputedFee(t *testing.T) {
117+
tx, _ := transaction.NewTransactionFromBEEFHex(BRC62Hex)
118+
119+
tx.AddOutput(&transaction.TransactionOutput{
120+
Change: true,
121+
LockingScript: tx.Outputs[0].LockingScript,
122+
})
123+
124+
err := tx.Sign()
125+
require.Error(t, err)
126+
127+
err = tx.SignUnsigned()
128+
require.Error(t, err)
129+
}
130+
131+
func TestSignUnsigned(t *testing.T) {
132+
tx, err := transaction.NewTransactionFromBEEFHex(BRC62Hex)
133+
require.NoError(t, err)
134+
135+
cloneTx := tx.Clone()
136+
pk, _ := ec.NewPrivateKey()
137+
138+
// Adding a script template with random key so sigs will be different
139+
for i := range tx.Inputs {
140+
cloneTx.Inputs[i].UnlockingScriptTemplate, err = p2pkh.Unlock(pk, nil)
141+
require.NoError(t, err)
142+
}
143+
144+
// This should do nothing because the inputs from hex are already signed
145+
err = cloneTx.SignUnsigned()
146+
require.NoError(t, err)
147+
for i := range cloneTx.Inputs {
148+
require.Equal(t, tx.Inputs[i].UnlockingScript, cloneTx.Inputs[i].UnlockingScript)
149+
}
150+
151+
// This should sign the inputs with the incorrect key which should change the sigs
152+
cloneTx.Sign()
153+
for i := range tx.Inputs {
154+
require.NotEqual(t, tx.Inputs[i].UnlockingScript, cloneTx.Inputs[i].UnlockingScript)
155+
}
156+
}
157+
158+
func TestSignUnsignedNew(t *testing.T) {
159+
pk, _ := ec.PrivateKeyFromWif("L1y6DgX4TuonxXzRPuk9reK2TD2THjwQReNUwVrvWN3aRkjcbauB")
160+
address, _ := script.NewAddressFromPublicKey(pk.PubKey(), true)
161+
tx := transaction.NewTransaction()
162+
lockingScript, err := p2pkh.Lock(address)
163+
require.NoError(t, err)
164+
sourceTxID, _ := chainhash.NewHashFromHex("fe77aa03d5563d3ec98455a76655ea3b58e19a4eb102baf7b2a47af37e94b295")
165+
unlockingScript, _ := p2pkh.Unlock(pk, nil)
166+
tx.AddInput(&transaction.TransactionInput{
167+
SourceTransaction: &transaction.Transaction{
168+
Outputs: []*transaction.TransactionOutput{
169+
{
170+
Satoshis: 1,
171+
LockingScript: lockingScript,
172+
},
173+
},
174+
},
175+
SourceTXID: sourceTxID,
176+
UnlockingScriptTemplate: unlockingScript,
177+
})
178+
179+
tx.AddOutput(&transaction.TransactionOutput{
180+
Satoshis: 1,
181+
LockingScript: lockingScript,
182+
})
183+
184+
err = tx.SignUnsigned()
185+
require.NoError(t, err)
186+
187+
for _, input := range tx.Inputs {
188+
require.Positive(t, len(input.UnlockingScript.Bytes()))
189+
}
190+
}

0 commit comments

Comments
 (0)