forked from lightninglabs/taproot-assets
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tx_validator.go
225 lines (186 loc) · 7.14 KB
/
tx_validator.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
package taprootassets
import (
"context"
"errors"
"fmt"
"sort"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/commitment"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/tapdb"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapscript"
"github.com/lightninglabs/taproot-assets/vm"
)
const (
// medianTimeBlocks is the number of previous blocks which should be
// used to calculate the median time used to validate block timestamps.
medianTimeBlocks = 11
)
var (
// errTxNotfound is an error that is returned when a transaction
// couldn't be found in the proof file.
errTxNotFound = fmt.Errorf("transaction not found in proof file")
)
// ValidatorV0 is an implementation of the tapscript.TxValidator interface
// that supports Taproot Asset script version 0.
type ValidatorV0 struct{}
// Execute creates and runs an instance of the Taproot Asset script V0 VM.
func (v *ValidatorV0) Execute(newAsset *asset.Asset,
splitAssets []*commitment.SplitAsset, prevAssets commitment.InputSet,
chainLookup asset.ChainLookup) error {
verifyOpts := vm.WithChainLookup(chainLookup)
engine, err := vm.New(newAsset, splitAssets, prevAssets, verifyOpts)
if err != nil {
return err
}
if err = engine.Execute(); err != nil {
return err
}
return nil
}
// A compile time assertion to ensure ValidatorV0 meets the
// tapscript.TxValidator interface.
var _ tapscript.TxValidator = (*ValidatorV0)(nil)
// WitnessValidatorV0 is an implementation of the tapscript.WitnessValidator
// interface that supports Taproot Asset script version 0.
type WitnessValidatorV0 struct{}
// ValidateWitnesses validates the created witnesses of an asset transfer.
func (v *WitnessValidatorV0) ValidateWitnesses(newAsset *asset.Asset,
splitAssets []*commitment.SplitAsset,
prevAssets commitment.InputSet) error {
return vm.ValidateWitnesses(newAsset, splitAssets, prevAssets)
}
// A compile time assertion to ensure WitnessValidatorV0 meets the
// tapscript.WitnessValidator interface.
var _ tapscript.WitnessValidator = (*WitnessValidatorV0)(nil)
// ProofChainLookup is an implementation of the asset.ChainLookup interface
// that uses a proof file to look up block height information of previous inputs
// while validating proofs.
type ProofChainLookup struct {
chainBridge tapgarden.ChainBridge
assetStore *tapdb.AssetStore
proofFile *proof.File
}
// NewProofChainLookup creates a new ProofChainLookup instance.
func NewProofChainLookup(chainBridge tapgarden.ChainBridge,
assetStore *tapdb.AssetStore, proofFile *proof.File) *ProofChainLookup {
return &ProofChainLookup{
chainBridge: chainBridge,
assetStore: assetStore,
proofFile: proofFile,
}
}
// CurrentHeight returns the current height of the main chain.
func (l *ProofChainLookup) CurrentHeight(ctx context.Context) (uint32, error) {
return l.chainBridge.CurrentHeight(ctx)
}
// TxBlockHeight returns the block height that the given transaction was
// included in.
func (l *ProofChainLookup) TxBlockHeight(ctx context.Context,
txid chainhash.Hash) (uint32, error) {
// If we don't have a proof available as context, we can only look up
// the transaction in the database. Querying it on-chain would cause a
// re-scan which might be very time costly for light clients.
if l.proofFile == nil || l.proofFile.NumProofs() == 0 {
return l.assetStore.TxHeight(ctx, txid)
}
// Let's walk back the proof chain and try to find the transaction.
height, err := findTxHeightInProofFile(l.proofFile, txid)
switch {
case errors.Is(err, errTxNotFound):
// Our last ditch attempt is to look up the transaction in the
// database. But we might not have it there if the proof is for
// a transaction that happened before the asset reached our
// node.
return l.assetStore.TxHeight(ctx, txid)
case err != nil:
return 0, fmt.Errorf("error fetching proof from context file: "+
"%w", err)
}
return height, nil
}
// findTxHeightInProofFile is a helper function that recursively searches for
// the block height of a transaction in a proof file.
func findTxHeightInProofFile(f *proof.File, txid chainhash.Hash) (uint32,
error) {
for i := f.NumProofs() - 1; i >= 0; i-- {
p, err := f.ProofAt(uint32(i))
if err != nil {
return 0, fmt.Errorf("error fetching proof from "+
"file: %w", err)
}
if p.AnchorTx.TxHash() == txid {
return p.BlockHeight, nil
}
for idx := range p.AdditionalInputs {
additionalInput := p.AdditionalInputs[idx]
height, err := findTxHeightInProofFile(
&additionalInput, txid,
)
switch {
case errors.Is(err, errTxNotFound):
continue
case err != nil:
return 0, fmt.Errorf("error fetching proof "+
"from additional input file: %w", err)
}
return height, nil
}
}
// If we arrive here, we couldn't find the transaction in the proof
// file.
return 0, errTxNotFound
}
// MeanBlockTimestamp returns the timestamp of the block at the given height as
// a Unix timestamp in seconds, taking into account the mean time elapsed over
// the previous 11 blocks.
func (l *ProofChainLookup) MeanBlockTimestamp(ctx context.Context,
blockHeight uint32) (time.Time, error) {
// Create a slice of the previous few block timestamps used to calculate
// the median per the number defined by the constant medianTimeBlocks.
//
// NOTE: The code below is an adaptation of the code in btcd's
// blockchain.CalcPastMedianTime function.
timestamps := make([]int64, medianTimeBlocks)
numNodes := 0
for i := uint32(0); i < medianTimeBlocks; i++ {
// If we have reached the beginning of the blockchain, we can't
// go back any further. This also prevents an underflow in the
// next step.
if i > blockHeight {
break
}
unixTs := l.chainBridge.GetBlockTimestamp(ctx, blockHeight-i)
if unixTs == 0 {
return time.Time{}, fmt.Errorf("couldn't find "+
"timestamp for block height %d", blockHeight)
}
timestamps[i] = unixTs
numNodes++
}
// Prune the slice to the actual number of available timestamps which
// will be fewer than desired near the beginning of the blockchain and
// sort them.
timestamps = timestamps[:numNodes]
sort.Slice(timestamps, func(i, j int) bool {
return timestamps[i] < timestamps[j]
})
// NOTE: The consensus rules incorrectly calculate the median for even
// numbers of blocks. A true median averages the middle two elements
// for a set with an even number of elements in it. Since the constant
// for the previous number of blocks to be used is odd, this is only an
// issue for a few blocks near the beginning of the chain. I suspect
// this is an optimization even though the result is slightly wrong for
// a few of the first blocks since after the first few blocks, there
// will always be an odd number of blocks in the set per the constant.
//
// This code follows suit to ensure the same rules are used, however, be
// aware that should the medianTimeBlocks constant ever be changed to an
// even number, this code will be wrong.
medianTimestamp := timestamps[numNodes/2]
return time.Unix(medianTimestamp, 0), nil
}
var _ asset.ChainLookup = (*ProofChainLookup)(nil)