-
Notifications
You must be signed in to change notification settings - Fork 176
/
identifiable_staking.go
480 lines (395 loc) · 13.2 KB
/
identifiable_staking.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
package btcstaking
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"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/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)
const (
// length of tag prefix indentifying staking transactions
TagLen = 4
// 4 bytes tag + 1 byte version + 32 bytes staker public key + 32 bytes finality provider public key + 2 bytes staking time
V0OpReturnDataSize = 71
v0OpReturnCreationErrMsg = "cannot create V0 op_return data"
)
type IdentifiableStakingInfo struct {
StakingOutput *wire.TxOut
scriptHolder *taprootScriptHolder
timeLockPathLeafHash chainhash.Hash
unbondingPathLeafHash chainhash.Hash
slashingPathLeafHash chainhash.Hash
OpReturnOutput *wire.TxOut
}
func uint16ToBytes(v uint16) []byte {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], v)
return buf[:]
}
func uint16FromBytes(b []byte) (uint16, error) {
if len(b) != 2 {
return 0, fmt.Errorf("invalid uint16 bytes length: %d", len(b))
}
return binary.BigEndian.Uint16(b), nil
}
// V0OpReturnData represents the data that is embedded in the OP_RETURN output
// It marshalls to exactly 71 bytes
type V0OpReturnData struct {
Tag []byte
Version byte
StakerPublicKey *XonlyPubKey
FinalityProviderPublicKey *XonlyPubKey
StakingTime uint16
}
func NewV0OpReturnData(
tag []byte,
stakerPublicKey []byte,
finalityProviderPublicKey []byte,
stakingTime []byte,
) (*V0OpReturnData, error) {
if len(tag) != TagLen {
return nil, fmt.Errorf("%s: invalid tag length: %d, expected: %d", v0OpReturnCreationErrMsg, len(tag), TagLen)
}
stakerKey, err := XOnlyPublicKeyFromBytes(stakerPublicKey)
if err != nil {
return nil, fmt.Errorf("%s:invalid staker public key:%w", v0OpReturnCreationErrMsg, err)
}
fpKey, err := XOnlyPublicKeyFromBytes(finalityProviderPublicKey)
if err != nil {
return nil, fmt.Errorf("%s:invalid finality provider public key:%w", v0OpReturnCreationErrMsg, err)
}
stakingTimeValue, err := uint16FromBytes(stakingTime)
if err != nil {
return nil, fmt.Errorf("%s:invalid staking time:%w", v0OpReturnCreationErrMsg, err)
}
return NewV0OpReturnDataFromParsed(tag, stakerKey.PubKey, fpKey.PubKey, stakingTimeValue)
}
func NewV0OpReturnDataFromParsed(
tag []byte,
stakerPublicKey *btcec.PublicKey,
finalityProviderPublicKey *btcec.PublicKey,
stakingTime uint16,
) (*V0OpReturnData, error) {
if len(tag) != TagLen {
return nil, fmt.Errorf("%s:invalid tag length: %d, expected: %d", v0OpReturnCreationErrMsg, len(tag), TagLen)
}
if stakerPublicKey == nil {
return nil, fmt.Errorf("%s:nil staker public key", v0OpReturnCreationErrMsg)
}
if finalityProviderPublicKey == nil {
return nil, fmt.Errorf("%s: nil finality provider public key", v0OpReturnCreationErrMsg)
}
return &V0OpReturnData{
Tag: tag,
Version: 0,
StakerPublicKey: &XonlyPubKey{stakerPublicKey},
FinalityProviderPublicKey: &XonlyPubKey{finalityProviderPublicKey},
StakingTime: stakingTime,
}, nil
}
func NewV0OpReturnDataFromBytes(b []byte) (*V0OpReturnData, error) {
if len(b) != V0OpReturnDataSize {
return nil, fmt.Errorf("invalid op return data length: %d, expected: %d", len(b), V0OpReturnDataSize)
}
tag := b[:TagLen]
version := b[TagLen]
if version != 0 {
return nil, fmt.Errorf("invalid op return version: %d, expected: %d", version, 0)
}
stakerPublicKey := b[TagLen+1 : TagLen+1+schnorr.PubKeyBytesLen]
finalityProviderPublicKey := b[TagLen+1+schnorr.PubKeyBytesLen : TagLen+1+schnorr.PubKeyBytesLen*2]
stakingTime := b[TagLen+1+schnorr.PubKeyBytesLen*2:]
return NewV0OpReturnData(tag, stakerPublicKey, finalityProviderPublicKey, stakingTime)
}
func getV0OpReturnBytes(out *wire.TxOut) ([]byte, error) {
if out == nil {
return nil, fmt.Errorf("nil tx output")
}
// We are adding `+2` as each op return has additional 2 for:
// 1. OP_RETURN opcode - which signalizes that data is provably unspendable
// 2. OP_DATA_71 opcode - which pushes 71 bytes of data to the stack
if len(out.PkScript) != V0OpReturnDataSize+2 {
return nil, fmt.Errorf("invalid op return data length: %d, expected: %d", len(out.PkScript), V0OpReturnDataSize+2)
}
if !txscript.IsNullData(out.PkScript) {
return nil, fmt.Errorf("invalid op return script")
}
return out.PkScript[2:], nil
}
func NewV0OpReturnDataFromTxOutput(out *wire.TxOut) (*V0OpReturnData, error) {
data, err := getV0OpReturnBytes(out)
if err != nil {
return nil, fmt.Errorf("cannot parse op return data: %w", err)
}
return NewV0OpReturnDataFromBytes(data)
}
func (d *V0OpReturnData) Marshall() []byte {
var data []byte
data = append(data, d.Tag...)
data = append(data, d.Version)
data = append(data, d.StakerPublicKey.Marshall()...)
data = append(data, d.FinalityProviderPublicKey.Marshall()...)
data = append(data, uint16ToBytes(d.StakingTime)...)
return data
}
func (d *V0OpReturnData) ToTxOutput() (*wire.TxOut, error) {
dataScript, err := txscript.NullDataScript(d.Marshall())
if err != nil {
return nil, err
}
return wire.NewTxOut(0, dataScript), nil
}
// BuildV0IdentifiableStakingOutputs creates outputs which every staking transaction must have
func BuildV0IdentifiableStakingOutputs(
tag []byte,
stakerKey *btcec.PublicKey,
fpKey *btcec.PublicKey,
covenantKeys []*btcec.PublicKey,
covenantQuorum uint32,
stakingTime uint16,
stakingAmount btcutil.Amount,
net *chaincfg.Params,
) (*IdentifiableStakingInfo, error) {
info, err := BuildStakingInfo(
stakerKey,
[]*btcec.PublicKey{fpKey},
covenantKeys,
covenantQuorum,
stakingTime,
stakingAmount,
net,
)
if err != nil {
return nil, err
}
opReturnData, err := NewV0OpReturnDataFromParsed(tag, stakerKey, fpKey, stakingTime)
if err != nil {
return nil, err
}
dataOutput, err := opReturnData.ToTxOutput()
if err != nil {
return nil, err
}
return &IdentifiableStakingInfo{
StakingOutput: info.StakingOutput,
scriptHolder: info.scriptHolder,
timeLockPathLeafHash: info.timeLockPathLeafHash,
unbondingPathLeafHash: info.unbondingPathLeafHash,
slashingPathLeafHash: info.slashingPathLeafHash,
OpReturnOutput: dataOutput,
}, nil
}
// BuildV0IdentifiableStakingOutputsAndTx creates outputs which every staking transaction must have and
// returns the not-funded transaction with these outputs
func BuildV0IdentifiableStakingOutputsAndTx(
tag []byte,
stakerKey *btcec.PublicKey,
fpKey *btcec.PublicKey,
covenantKeys []*btcec.PublicKey,
covenantQuorum uint32,
stakingTime uint16,
stakingAmount btcutil.Amount,
net *chaincfg.Params,
) (*IdentifiableStakingInfo, *wire.MsgTx, error) {
info, err := BuildV0IdentifiableStakingOutputs(
tag,
stakerKey,
fpKey,
covenantKeys,
covenantQuorum,
stakingTime,
stakingAmount,
net,
)
if err != nil {
return nil, nil, err
}
tx := wire.NewMsgTx(2)
tx.AddTxOut(info.StakingOutput)
tx.AddTxOut(info.OpReturnOutput)
return info, tx, nil
}
func (i *IdentifiableStakingInfo) TimeLockPathSpendInfo() (*SpendInfo, error) {
return i.scriptHolder.scriptSpendInfoByName(i.timeLockPathLeafHash)
}
func (i *IdentifiableStakingInfo) UnbondingPathSpendInfo() (*SpendInfo, error) {
return i.scriptHolder.scriptSpendInfoByName(i.unbondingPathLeafHash)
}
func (i *IdentifiableStakingInfo) SlashingPathSpendInfo() (*SpendInfo, error) {
return i.scriptHolder.scriptSpendInfoByName(i.slashingPathLeafHash)
}
type ParsedV0StakingTx struct {
StakingOutput *wire.TxOut
StakingOutputIdx int
OpReturnOutput *wire.TxOut
OpReturnOutputIdx int
OpReturnData *V0OpReturnData
}
func tryToGetOpReturnDataFromOutputs(outputs []*wire.TxOut) (*V0OpReturnData, int, error) {
// lack of outputs is not an error
if len(outputs) == 0 {
return nil, -1, nil
}
var opReturnData *V0OpReturnData
var opReturnOutputIdx int
for i, o := range outputs {
output := o
d, err := NewV0OpReturnDataFromTxOutput(output)
if err != nil {
// this is not an op return output recognized by Babylon, move forward
continue
}
// this case should not happen as standard bitcoin node propagation rules
// disallow multiple op return outputs in a single transaction. However, miner could
// include multiple op return outputs in a single transaction. In such case, we should
// return an error.
if opReturnData != nil {
return nil, -1, fmt.Errorf("multiple op return outputs found")
}
opReturnData = d
opReturnOutputIdx = i
}
return opReturnData, opReturnOutputIdx, nil
}
func tryToGetStakingOutput(outputs []*wire.TxOut, stakingOutputPkScript []byte) (*wire.TxOut, int, error) {
// lack of outputs is not an error
if len(outputs) == 0 {
return nil, -1, nil
}
var stakingOutput *wire.TxOut
var stakingOutputIdx int
for i, o := range outputs {
output := o
if !bytes.Equal(output.PkScript, stakingOutputPkScript) {
// this is not the staking output we are looking for
continue
}
if stakingOutput != nil {
// we only allow for one staking output per transaction
return nil, -1, fmt.Errorf("multiple staking outputs found")
}
stakingOutput = output
stakingOutputIdx = i
}
return stakingOutput, stakingOutputIdx, nil
}
// ParseV0StakingTx takes a btc transaction and checks whether it is a staking transaction and if so parses it
// for easy data retrieval.
// It does all necessary checks to ensure that the transaction is valid staking transaction.
func ParseV0StakingTx(
tx *wire.MsgTx,
expectedTag []byte,
covenantKeys []*btcec.PublicKey,
covenantQuorum uint32,
net *chaincfg.Params,
) (*ParsedV0StakingTx, error) {
// 1. Basic arguments checks
if tx == nil {
return nil, fmt.Errorf("nil tx")
}
if len(expectedTag) != TagLen {
return nil, fmt.Errorf("invalid tag length: %d, expected: %d", len(expectedTag), TagLen)
}
if len(covenantKeys) == 0 {
return nil, fmt.Errorf("no covenant keys specified")
}
if covenantQuorum > uint32(len(covenantKeys)) {
return nil, fmt.Errorf("covenant quorum is greater than the number of covenant keys")
}
// 2. Identify whether the transaction has expected shape
if len(tx.TxOut) < 2 {
return nil, fmt.Errorf("staking tx must have at least 2 outputs")
}
opReturnData, opReturnOutputIdx, err := tryToGetOpReturnDataFromOutputs(tx.TxOut)
if err != nil {
return nil, fmt.Errorf("cannot parse staking transaction: %w", err)
}
if opReturnData == nil {
return nil, fmt.Errorf("transaction does not have expected op return output")
}
// at this point we know that transaction has op return output which seems to match
// the expected shape. Check the tag and version.
if !bytes.Equal(opReturnData.Tag, expectedTag) {
return nil, fmt.Errorf("unexpected tag: %s, expected: %s",
hex.EncodeToString(opReturnData.Tag),
hex.EncodeToString(expectedTag),
)
}
if opReturnData.Version != 0 {
return nil, fmt.Errorf("unexpcted version: %d, expected: %d", opReturnData.Version, 0)
}
// 3. Op return seems to be valid V0 op return output. Now, we need to check whether
// the staking output exists and is valid.
stakingInfo, err := BuildStakingInfo(
opReturnData.StakerPublicKey.PubKey,
[]*btcec.PublicKey{opReturnData.FinalityProviderPublicKey.PubKey},
covenantKeys,
covenantQuorum,
opReturnData.StakingTime,
// we can pass 0 here, as staking amount is not used when creating taproot address
0,
net,
)
if err != nil {
return nil, fmt.Errorf("cannot build staking info: %w", err)
}
stakingOutput, stakingOutputIdx, err := tryToGetStakingOutput(tx.TxOut, stakingInfo.StakingOutput.PkScript)
if err != nil {
return nil, fmt.Errorf("cannot parse staking transaction: %w", err)
}
if stakingOutput == nil {
return nil, fmt.Errorf("staking output not found in potential staking transaction")
}
return &ParsedV0StakingTx{
StakingOutput: stakingOutput,
StakingOutputIdx: stakingOutputIdx,
OpReturnOutput: tx.TxOut[opReturnOutputIdx],
OpReturnOutputIdx: opReturnOutputIdx,
OpReturnData: opReturnData,
}, nil
}
// IsPossibleV0StakingTx checks whether transaction may be a valid staking transaction
// checks:
// 1. Whether the transaction has at least 2 outputs
// 2. have an op return output
// 3. op return output has expected tag
// This function is much faster than ParseV0StakingTx, as it does not perform
// all necessary checks.
func IsPossibleV0StakingTx(tx *wire.MsgTx, expectedTag []byte) bool {
if len(expectedTag) != TagLen {
return false
}
if len(tx.TxOut) < 2 {
return false
}
var possibleStakingTx = false
for _, o := range tx.TxOut {
output := o
data, err := getV0OpReturnBytes(output)
if err != nil {
// this is not an op return output recognized by Babylon, move forward
continue
}
if !bytes.Equal(data[:TagLen], expectedTag) {
// this is not the op return output we are looking for as tag do not match
continue
}
if data[TagLen] != 0 {
// this is not the v0 op return output
continue
}
if possibleStakingTx {
// this is second output that matches the tag, we do not allow for multiple op return outputs
// so this is not a valid staking transaction
return false
}
possibleStakingTx = true
}
return possibleStakingTx
}