-
Notifications
You must be signed in to change notification settings - Fork 0
/
transaction_in_context.go
362 lines (307 loc) · 12.5 KB
/
transaction_in_context.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
package transactionvalidator
import (
"math"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/model"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/model/externalapi"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/ruleerrors"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/utils/consensushashing"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/utils/constants"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/utils/transactionhelper"
"github.com/karlsend/PYVERT/testfork/karlsend/domain/consensus/utils/txscript"
"github.com/pkg/errors"
)
// IsFinalizedTransaction determines whether or not a transaction is finalized.
func (v *transactionValidator) IsFinalizedTransaction(tx *externalapi.DomainTransaction, blockDAAScore uint64, blockTime int64) bool {
// Lock time of zero means the transaction is finalized.
lockTime := tx.LockTime
if lockTime == 0 {
return true
}
// The lock time field of a transaction is either a block DAA score at
// which the transaction is finalized or a timestamp depending on if the
// value is before the constants.LockTimeThreshold. When it is under the
// threshold it is a DAA score.
blockTimeOrBlueScore := uint64(0)
if lockTime < constants.LockTimeThreshold {
blockTimeOrBlueScore = blockDAAScore
} else {
blockTimeOrBlueScore = uint64(blockTime)
}
if lockTime < blockTimeOrBlueScore {
return true
}
// At this point, the transaction's lock time hasn't occurred yet, but
// the transaction might still be finalized if the sequence number
// for all transaction inputs is maxed out.
for _, input := range tx.Inputs {
if input.Sequence != math.MaxUint64 {
return false
}
}
return true
}
// ValidateTransactionInContextIgnoringUTXO validates the transaction with consensus context but ignoring UTXO
func (v *transactionValidator) ValidateTransactionInContextIgnoringUTXO(stagingArea *model.StagingArea, tx *externalapi.DomainTransaction,
povBlockHash *externalapi.DomainHash, povBlockPastMedianTime int64) error {
povBlockDAAScore, err := v.daaBlocksStore.DAAScore(v.databaseContext, stagingArea, povBlockHash)
if err != nil {
return err
}
if isFinalized := v.IsFinalizedTransaction(tx, povBlockDAAScore, povBlockPastMedianTime); !isFinalized {
return errors.Wrapf(ruleerrors.ErrUnfinalizedTx, "unfinalized transaction %v", tx)
}
return nil
}
// ValidateTransactionInContextAndPopulateFee validates the transaction against its referenced UTXO, and
// populates its fee field.
//
// Note: if the function fails, there's no guarantee that the transaction fee field will remain unaffected.
func (v *transactionValidator) ValidateTransactionInContextAndPopulateFee(stagingArea *model.StagingArea,
tx *externalapi.DomainTransaction, povBlockHash *externalapi.DomainHash) error {
err := v.checkTransactionCoinbaseMaturity(stagingArea, povBlockHash, tx)
if err != nil {
return err
}
totalSompiIn, err := v.checkTransactionInputAmounts(tx)
if err != nil {
return err
}
totalSompiOut, err := v.checkTransactionOutputAmounts(tx, totalSompiIn)
if err != nil {
return err
}
tx.Fee = totalSompiIn - totalSompiOut
err = v.checkTransactionSequenceLock(stagingArea, povBlockHash, tx)
if err != nil {
return err
}
err = v.validateTransactionSigOpCounts(tx)
if err != nil {
return err
}
err = v.validateTransactionScripts(tx)
if err != nil {
return err
}
return nil
}
func (v *transactionValidator) checkTransactionCoinbaseMaturity(stagingArea *model.StagingArea,
povBlockHash *externalapi.DomainHash, tx *externalapi.DomainTransaction) error {
povDAAScore, err := v.daaBlocksStore.DAAScore(v.databaseContext, stagingArea, povBlockHash)
if err != nil {
return err
}
var missingOutpoints []*externalapi.DomainOutpoint
for _, input := range tx.Inputs {
utxoEntry := input.UTXOEntry
if utxoEntry == nil {
missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint)
} else if utxoEntry.IsCoinbase() {
originDAAScore := utxoEntry.BlockDAAScore()
if originDAAScore+v.blockCoinbaseMaturity > povDAAScore {
return errors.Wrapf(ruleerrors.ErrImmatureSpend, "tried to spend coinbase "+
"transaction output %s from DAA score %d "+
"to DAA score %d before required maturity "+
"of %d", input.PreviousOutpoint,
originDAAScore, povDAAScore,
v.blockCoinbaseMaturity)
}
}
}
if len(missingOutpoints) > 0 {
return ruleerrors.NewErrMissingTxOut(missingOutpoints)
}
return nil
}
func (v *transactionValidator) checkTransactionInputAmounts(tx *externalapi.DomainTransaction) (totalSompiIn uint64, err error) {
totalSompiIn = 0
var missingOutpoints []*externalapi.DomainOutpoint
for _, input := range tx.Inputs {
utxoEntry := input.UTXOEntry
if utxoEntry == nil {
missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint)
continue
}
// Ensure the transaction amounts are in range. Each of the
// output values of the input transactions must not be negative
// or more than the max allowed per transaction. All amounts in
// a transaction are in a unit value known as a sompi. One
// kaspa is a quantity of sompi as defined by the
// SompiPerKaspa constant.
totalSompiIn, err = v.checkEntryAmounts(utxoEntry, totalSompiIn)
if err != nil {
return 0, err
}
}
if len(missingOutpoints) > 0 {
return 0, ruleerrors.NewErrMissingTxOut(missingOutpoints)
}
return totalSompiIn, nil
}
func (v *transactionValidator) checkEntryAmounts(entry externalapi.UTXOEntry, totalSompiInBefore uint64) (totalSompiInAfter uint64, err error) {
// The total of all outputs must not be more than the max
// allowed per transaction. Also, we could potentially overflow
// the accumulator so check for overflow.
originTxSompi := entry.Amount()
totalSompiInAfter = totalSompiInBefore + originTxSompi
if totalSompiInAfter < totalSompiInBefore ||
totalSompiInAfter > constants.MaxSompi {
return 0, errors.Wrapf(ruleerrors.ErrBadTxOutValue, "total value of all transaction "+
"inputs is %d which is higher than max "+
"allowed value of %d", totalSompiInBefore,
constants.MaxSompi)
}
return totalSompiInAfter, nil
}
func (v *transactionValidator) checkTransactionOutputAmounts(tx *externalapi.DomainTransaction, totalSompiIn uint64) (uint64, error) {
totalSompiOut := uint64(0)
// Calculate the total output amount for this transaction. It is safe
// to ignore overflow and out of range errors here because those error
// conditions would have already been caught by checkTransactionAmountRanges.
for _, output := range tx.Outputs {
totalSompiOut += output.Value
}
// Ensure the transaction does not spend more than its inputs.
if totalSompiIn < totalSompiOut {
return 0, errors.Wrapf(ruleerrors.ErrSpendTooHigh, "total value of all transaction inputs for "+
"the transaction is %d which is less than the amount "+
"spent of %d", totalSompiIn, totalSompiOut)
}
return totalSompiOut, nil
}
func (v *transactionValidator) checkTransactionSequenceLock(stagingArea *model.StagingArea,
povBlockHash *externalapi.DomainHash, tx *externalapi.DomainTransaction) error {
// A transaction can only be included within a block
// once the sequence locks of *all* its inputs are
// active.
sequenceLock, err := v.calcTxSequenceLockFromReferencedUTXOEntries(stagingArea, povBlockHash, tx)
if err != nil {
return err
}
daaScore, err := v.daaBlocksStore.DAAScore(v.databaseContext, stagingArea, povBlockHash)
if err != nil {
return err
}
if !v.sequenceLockActive(sequenceLock, daaScore) {
return errors.Wrapf(ruleerrors.ErrUnfinalizedTx, "block contains "+
"transaction whose input sequence "+
"locks are not met")
}
return nil
}
func (v *transactionValidator) validateTransactionScripts(tx *externalapi.DomainTransaction) error {
var missingOutpoints []*externalapi.DomainOutpoint
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, input := range tx.Inputs {
// Create a new script engine for the script pair.
sigScript := input.SignatureScript
utxoEntry := input.UTXOEntry
if utxoEntry == nil {
missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint)
continue
}
scriptPubKey := utxoEntry.ScriptPublicKey()
vm, err := txscript.NewEngine(scriptPubKey, tx, i, txscript.ScriptNoFlags, v.sigCache, v.sigCacheECDSA, sighashReusedValues)
if err != nil {
return errors.Wrapf(ruleerrors.ErrScriptMalformed, "failed to parse input "+
"%d which references output %s - "+
"%s (input script bytes %x, prev "+
"output script bytes %x)",
i,
input.PreviousOutpoint, err, sigScript, scriptPubKey)
}
// Execute the script pair.
if err := vm.Execute(); err != nil {
return errors.Wrapf(ruleerrors.ErrScriptValidation, "failed to validate input "+
"%d which references output %s - "+
"%s (input script bytes %x, prev output "+
"script bytes %x)",
i,
input.PreviousOutpoint, err, sigScript, scriptPubKey)
}
}
if len(missingOutpoints) > 0 {
return ruleerrors.NewErrMissingTxOut(missingOutpoints)
}
return nil
}
func (v *transactionValidator) calcTxSequenceLockFromReferencedUTXOEntries(stagingArea *model.StagingArea,
povBlockHash *externalapi.DomainHash, tx *externalapi.DomainTransaction) (*sequenceLock, error) {
// A value of -1 represents a relative timelock value that will allow a transaction to be
//included in a block at any given DAA score.
sequenceLock := &sequenceLock{BlockDAAScore: -1}
// Sequence locks don't apply to coinbase transactions Therefore, we
// return sequence lock values of -1 indicating that this transaction
// can be included within a block at any given DAA score.
if transactionhelper.IsCoinBase(tx) {
return sequenceLock, nil
}
var missingOutpoints []*externalapi.DomainOutpoint
for _, input := range tx.Inputs {
utxoEntry := input.UTXOEntry
if utxoEntry == nil {
missingOutpoints = append(missingOutpoints, &input.PreviousOutpoint)
continue
}
inputDAAScore := utxoEntry.BlockDAAScore()
// Given a sequence number, we apply the relative time lock
// mask in order to obtain the time lock delta required before
// this input can be spent.
sequenceNum := input.Sequence
relativeLock := int64(sequenceNum & constants.SequenceLockTimeMask)
// Relative time locks are disabled for this input, so we can
// skip any further calculation.
if sequenceNum&constants.SequenceLockTimeDisabled == constants.SequenceLockTimeDisabled {
continue
}
// The relative lock-time for this input is expressed
// in blocks so we calculate the relative offset from
// the input's DAA score as its converted absolute
// lock-time. We subtract one from the relative lock in
// order to maintain the original lockTime semantics.
blockDAAScore := int64(inputDAAScore) + relativeLock - 1
if blockDAAScore > sequenceLock.BlockDAAScore {
sequenceLock.BlockDAAScore = blockDAAScore
}
}
if len(missingOutpoints) > 0 {
return nil, ruleerrors.NewErrMissingTxOut(missingOutpoints)
}
return sequenceLock, nil
}
// sequenceLock represents the converted relative lock-time in
// absolute block-daa-score for a transaction input's relative lock-times.
// According to sequenceLock, after the referenced input has been confirmed
// within a block, a transaction spending that input can be included into a
// block either after the 'BlockDAAScore' has been reached.
type sequenceLock struct {
BlockDAAScore int64
}
// sequenceLockActive determines if a transaction's sequence locks have been
// met, meaning that all the inputs of a given transaction have reached a
// DAA score sufficient for their relative lock-time maturity.
func (v *transactionValidator) sequenceLockActive(sequenceLock *sequenceLock, blockDAAScore uint64) bool {
// If (DAA score) relative-lock time has not yet
// reached, then the transaction is not yet mature according to its
// sequence locks.
if sequenceLock.BlockDAAScore >= int64(blockDAAScore) {
return false
}
return true
}
func (v *transactionValidator) validateTransactionSigOpCounts(tx *externalapi.DomainTransaction) error {
for i, input := range tx.Inputs {
utxoEntry := input.UTXOEntry
// Count the precise number of signature operations in the
// referenced public key script.
sigScript := input.SignatureScript
isP2SH := txscript.IsPayToScriptHash(utxoEntry.ScriptPublicKey())
sigOpCount := txscript.GetPreciseSigOpCount(sigScript, utxoEntry.ScriptPublicKey(), isP2SH)
if sigOpCount != int(input.SigOpCount) {
return errors.Wrapf(ruleerrors.ErrWrongSigOpCount,
"input %d specifies SigOpCount %d while actual SigOpCount is %d",
i, input.SigOpCount, sigOpCount)
}
}
return nil
}