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
|
package types
// validtransaction.go has functions for checking whether a transaction is
// valid outside of the context of a consensus set. This means checking the
// size of the transaction, the content of the signatures, and a large set of
// other rules that are inherent to how a transaction should be constructed.
import (
"errors"
)
var (
ErrDoubleSpend = errors.New("transaction uses a parent object twice")
ErrFileContractWindowEndViolation = errors.New("file contract window must end at least one block after it starts")
ErrFileContractWindowStartViolation = errors.New("file contract window must start in the future")
ErrFileContractOutputSumViolation = errors.New("file contract has invalid output sums")
ErrNonZeroClaimStart = errors.New("transaction has a siafund output with a non-zero siafund claim")
ErrNonZeroRevision = errors.New("new file contract has a nonzero revision number")
ErrStorageProofWithOutputs = errors.New("transaction has both a storage proof and other outputs")
ErrTimelockNotSatisfied = errors.New("timelock has not been met")
ErrTransactionTooLarge = errors.New("transaction is too large to fit in a block")
ErrZeroMinerFee = errors.New("transaction has a zero value miner fee")
ErrZeroOutput = errors.New("transaction cannot have an output or payout that has zero value")
ErrZeroRevision = errors.New("transaction has a file contract revision with RevisionNumber=0")
)
// correctFileContracts checks that the file contracts adhere to the file
// contract rules.
func (t Transaction) correctFileContracts(currentHeight BlockHeight) error {
// Check that FileContract rules are being followed.
for _, fc := range t.FileContracts {
// Check that start and expiration are reasonable values.
if fc.WindowStart <= currentHeight {
return ErrFileContractWindowStartViolation
}
if fc.WindowEnd <= fc.WindowStart {
return ErrFileContractWindowEndViolation
}
// Check that the proof outputs sum to the payout after the
// siafund fee has been applied.
var validProofOutputSum, missedProofOutputSum Currency
for _, output := range fc.ValidProofOutputs {
/* - Future hardforking code.
if output.Value.IsZero() {
return ErrZeroOutput
}
*/
validProofOutputSum = validProofOutputSum.Add(output.Value)
}
for _, output := range fc.MissedProofOutputs {
/* - Future hardforking code.
if output.Value.IsZero() {
return ErrZeroOutput
}
*/
missedProofOutputSum = missedProofOutputSum.Add(output.Value)
}
outputPortion := PostTax(currentHeight, fc.Payout)
if validProofOutputSum.Cmp(outputPortion) != 0 {
return ErrFileContractOutputSumViolation
}
if missedProofOutputSum.Cmp(outputPortion) != 0 {
return ErrFileContractOutputSumViolation
}
}
return nil
}
// correctFileContractRevisions checks that any file contract revisions adhere
// to the revision rules.
func (t Transaction) correctFileContractRevisions(currentHeight BlockHeight) error {
for _, fcr := range t.FileContractRevisions {
// Check that start and expiration are reasonable values.
if fcr.NewWindowStart <= currentHeight {
return ErrFileContractWindowStartViolation
}
if fcr.NewWindowEnd <= fcr.NewWindowStart {
return ErrFileContractWindowEndViolation
}
// Check that the valid outputs and missed outputs sum to the same
// value.
var validProofOutputSum, missedProofOutputSum Currency
for _, output := range fcr.NewValidProofOutputs {
/* - Future hardforking code.
if output.Value.IsZero() {
return ErrZeroOutput
}
*/
validProofOutputSum = validProofOutputSum.Add(output.Value)
}
for _, output := range fcr.NewMissedProofOutputs {
/* - Future hardforking code.
if output.Value.IsZero() {
return ErrZeroOutput
}
*/
missedProofOutputSum = missedProofOutputSum.Add(output.Value)
}
if validProofOutputSum.Cmp(missedProofOutputSum) != 0 {
return ErrFileContractOutputSumViolation
}
}
return nil
}
// fitsInABlock checks if the transaction is likely to fit in a block. After
// OakHardforkHeight, transactions must be smaller than 64 KiB.
func (t Transaction) fitsInABlock(currentHeight BlockHeight) error {
// Check that the transaction will fit inside of a block, leaving 5kb for
// overhead.
size := uint64(t.MarshalSiaSize())
if size > BlockSizeLimit-5e3 {
return ErrTransactionTooLarge
}
if currentHeight >= OakHardforkBlock {
if size > OakHardforkTxnSizeLimit {
return ErrTransactionTooLarge
}
}
return nil
}
// followsMinimumValues checks that all outputs adhere to the rules for the
// minimum allowed value (generally 1).
func (t Transaction) followsMinimumValues() error {
for _, sco := range t.SiacoinOutputs {
if sco.Value.IsZero() {
return ErrZeroOutput
}
}
for _, fc := range t.FileContracts {
if fc.Payout.IsZero() {
return ErrZeroOutput
}
}
for _, sfo := range t.SiafundOutputs {
// SiafundOutputs are special in that they have a reserved field, the
// ClaimStart, which gets sent over the wire but must always be set to
// 0. The Value must always be greater than 0.
if !sfo.ClaimStart.IsZero() {
return ErrNonZeroClaimStart
}
if sfo.Value.IsZero() {
return ErrZeroOutput
}
}
for _, fee := range t.MinerFees {
if fee.IsZero() {
return ErrZeroMinerFee
}
}
return nil
}
// FollowsStorageProofRules checks that a transaction follows the limitations
// placed on transactions that have storage proofs.
func (t Transaction) followsStorageProofRules() error {
// No storage proofs, no problems.
if len(t.StorageProofs) == 0 {
return nil
}
// If there are storage proofs, there can be no siacoin outputs, siafund
// outputs, new file contracts, or file contract terminations. These
// restrictions are in place because a storage proof can be invalidated by
// a simple reorg, which will also invalidate the rest of the transaction.
// These restrictions minimize blockchain turbulence. These other types
// cannot be invalidated by a simple reorg, and must instead by replaced by
// a conflicting transaction.
if len(t.SiacoinOutputs) != 0 {
return ErrStorageProofWithOutputs
}
if len(t.FileContracts) != 0 {
return ErrStorageProofWithOutputs
}
if len(t.FileContractRevisions) != 0 {
return ErrStorageProofWithOutputs
}
if len(t.SiafundOutputs) != 0 {
return ErrStorageProofWithOutputs
}
return nil
}
// noRepeats checks that a transaction does not spend multiple outputs twice,
// submit two valid storage proofs for the same file contract, etc. We
// frivolously check that a file contract termination and storage proof don't
// act on the same file contract. There is very little overhead for doing so,
// and the check is only frivolous because of the current rule that file
// contract terminations are not valid after the proof window opens.
func (t Transaction) noRepeats() error {
// Check that there are no repeat instances of siacoin outputs, storage
// proofs, contract terminations, or siafund outputs.
siacoinInputs := make(map[SiacoinOutputID]struct{})
for _, sci := range t.SiacoinInputs {
_, exists := siacoinInputs[sci.ParentID]
if exists {
return ErrDoubleSpend
}
siacoinInputs[sci.ParentID] = struct{}{}
}
doneFileContracts := make(map[FileContractID]struct{})
for _, sp := range t.StorageProofs {
_, exists := doneFileContracts[sp.ParentID]
if exists {
return ErrDoubleSpend
}
doneFileContracts[sp.ParentID] = struct{}{}
}
for _, fcr := range t.FileContractRevisions {
_, exists := doneFileContracts[fcr.ParentID]
if exists {
return ErrDoubleSpend
}
doneFileContracts[fcr.ParentID] = struct{}{}
}
siafundInputs := make(map[SiafundOutputID]struct{})
for _, sfi := range t.SiafundInputs {
_, exists := siafundInputs[sfi.ParentID]
if exists {
return ErrDoubleSpend
}
siafundInputs[sfi.ParentID] = struct{}{}
}
return nil
}
// validUnlockConditions checks that the conditions of uc have been met. The
// height is taken as input so that modules who might be at a different height
// can do the verification without needing to use their own function.
// Additionally, it means that the function does not need to be a method of the
// consensus set.
func validUnlockConditions(uc UnlockConditions, currentHeight BlockHeight) (err error) {
if uc.Timelock > currentHeight {
return ErrTimelockNotSatisfied
}
return
}
// validUnlockConditions checks that all of the unlock conditions in the
// transaction are valid.
func (t Transaction) validUnlockConditions(currentHeight BlockHeight) (err error) {
for _, sci := range t.SiacoinInputs {
err = validUnlockConditions(sci.UnlockConditions, currentHeight)
if err != nil {
return
}
}
for _, fcr := range t.FileContractRevisions {
err = validUnlockConditions(fcr.UnlockConditions, currentHeight)
if err != nil {
return
}
}
for _, sfi := range t.SiafundInputs {
err = validUnlockConditions(sfi.UnlockConditions, currentHeight)
if err != nil {
return
}
}
return
}
// StandaloneValid returns an error if a transaction is not valid in any
// context, for example if the same output is spent twice in the same
// transaction. StandaloneValid will not check that all outputs being spent are
// legal outputs, as it has no confirmed or unconfirmed set to look at.
func (t Transaction) StandaloneValid(currentHeight BlockHeight) (err error) {
err = t.fitsInABlock(currentHeight)
if err != nil {
return
}
err = t.followsStorageProofRules()
if err != nil {
return
}
err = t.noRepeats()
if err != nil {
return
}
err = t.followsMinimumValues()
if err != nil {
return
}
err = t.correctFileContracts(currentHeight)
if err != nil {
return
}
err = t.correctFileContractRevisions(currentHeight)
if err != nil {
return
}
err = t.validUnlockConditions(currentHeight)
if err != nil {
return
}
err = t.validSignatures(currentHeight)
if err != nil {
return
}
return
}
|