|
1 | 1 | package validator |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
4 | 5 | "testing" |
5 | 6 |
|
| 7 | + "github.com/bsv-blockchain/go-sdk/chainhash" |
| 8 | + "github.com/bsv-blockchain/go-sdk/script" |
| 9 | + sdkTx "github.com/bsv-blockchain/go-sdk/transaction" |
| 10 | + |
6 | 11 | arcerrors "github.com/bsv-blockchain/arcade/errors" |
7 | 12 | ) |
8 | 13 |
|
@@ -103,3 +108,172 @@ func TestWrapPolicyError_UnlockingScripts(t *testing.T) { |
103 | 108 | t.Errorf("expected StatusUnlockingScripts (461), got %d", arcErr.StatusCode) |
104 | 109 | } |
105 | 110 | } |
| 111 | + |
| 112 | +// nonDataLockingScript returns a minimal non-data locking script suitable for |
| 113 | +// exercising the output validation paths. A single OP_TRUE (0x51) is treated |
| 114 | +// as a non-data, non-empty locking script by the SDK's IsData heuristic. |
| 115 | +func nonDataLockingScript() *script.Script { |
| 116 | + s := script.Script([]byte{0x51}) |
| 117 | + return &s |
| 118 | +} |
| 119 | + |
| 120 | +// nonZeroSourceTXID returns a pointer to a non-zero chainhash so an input is |
| 121 | +// not treated as a coinbase input by checkInputs. |
| 122 | +func nonZeroSourceTXID() *chainhash.Hash { |
| 123 | + h := chainhash.Hash{} |
| 124 | + h[0] = 0x01 |
| 125 | + return &h |
| 126 | +} |
| 127 | + |
| 128 | +// TestCheckOutputs_OverflowGuarded crafts a transaction with two non-data |
| 129 | +// outputs each at maxSatoshis. Each value individually passes the |
| 130 | +// "output.Satoshis > maxSatoshis" check, and their sum exceeds maxSatoshis; |
| 131 | +// the per-iteration guard must reject this before the unbounded total can |
| 132 | +// be relied upon. |
| 133 | +func TestCheckOutputs_OverflowGuarded(t *testing.T) { |
| 134 | + v := NewValidator(nil, nil) |
| 135 | + |
| 136 | + tx := &sdkTx.Transaction{ |
| 137 | + Outputs: []*sdkTx.TransactionOutput{ |
| 138 | + {Satoshis: maxSatoshis, LockingScript: nonDataLockingScript()}, |
| 139 | + {Satoshis: maxSatoshis, LockingScript: nonDataLockingScript()}, |
| 140 | + }, |
| 141 | + } |
| 142 | + |
| 143 | + err := v.checkOutputs(tx) |
| 144 | + if err == nil { |
| 145 | + t.Fatal("expected error for total satoshis above maxSatoshis, got nil") |
| 146 | + } |
| 147 | + if !errors.Is(err, ErrTxOutputTotalSatoshisTooHigh) { |
| 148 | + t.Errorf("expected ErrTxOutputTotalSatoshisTooHigh, got %v", err) |
| 149 | + } |
| 150 | + if !errors.Is(err, ErrTxOutputInvalid) { |
| 151 | + t.Errorf("expected ErrTxOutputInvalid wrap, got %v", err) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +// TestCheckOutputs_ManySmallOutputsCannotOverflow exercises the per-iteration |
| 156 | +// guard by accumulating many outputs whose total exceeds maxSatoshis. This is |
| 157 | +// the regression case for F-004: prior to the fix a sequence of values that |
| 158 | +// summed past 2^64 could wrap and slip past the post-loop check. |
| 159 | +func TestCheckOutputs_ManySmallOutputsCannotOverflow(t *testing.T) { |
| 160 | + v := NewValidator(nil, nil) |
| 161 | + |
| 162 | + half := uint64(maxSatoshis / 2) |
| 163 | + tx := &sdkTx.Transaction{ |
| 164 | + Outputs: []*sdkTx.TransactionOutput{ |
| 165 | + {Satoshis: half, LockingScript: nonDataLockingScript()}, |
| 166 | + {Satoshis: half, LockingScript: nonDataLockingScript()}, |
| 167 | + {Satoshis: half, LockingScript: nonDataLockingScript()}, |
| 168 | + }, |
| 169 | + } |
| 170 | + |
| 171 | + err := v.checkOutputs(tx) |
| 172 | + if err == nil { |
| 173 | + t.Fatal("expected error for cumulative output satoshis above maxSatoshis, got nil") |
| 174 | + } |
| 175 | + if !errors.Is(err, ErrTxOutputTotalSatoshisTooHigh) { |
| 176 | + t.Errorf("expected ErrTxOutputTotalSatoshisTooHigh, got %v", err) |
| 177 | + } |
| 178 | +} |
| 179 | + |
| 180 | +// TestCheckOutputs_ValidPasses confirms a legitimate transaction is still |
| 181 | +// accepted by checkOutputs after the overflow guard is added. |
| 182 | +func TestCheckOutputs_ValidPasses(t *testing.T) { |
| 183 | + v := NewValidator(nil, nil) |
| 184 | + |
| 185 | + tx := &sdkTx.Transaction{ |
| 186 | + Outputs: []*sdkTx.TransactionOutput{ |
| 187 | + {Satoshis: 1_000, LockingScript: nonDataLockingScript()}, |
| 188 | + {Satoshis: 2_000, LockingScript: nonDataLockingScript()}, |
| 189 | + }, |
| 190 | + } |
| 191 | + |
| 192 | + if err := v.checkOutputs(tx); err != nil { |
| 193 | + t.Fatalf("expected no error for valid outputs, got %v", err) |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +// TestCheckInputs_OverflowGuarded crafts a transaction whose input |
| 198 | +// SourceTxSatoshis values are each <= maxSatoshis but whose cumulative sum |
| 199 | +// exceeds maxSatoshis. Without the per-iteration guard a uint64 wrap could |
| 200 | +// allow the post-loop check to pass. |
| 201 | +func TestCheckInputs_OverflowGuarded(t *testing.T) { |
| 202 | + v := NewValidator(nil, nil) |
| 203 | + |
| 204 | + makeInput := func(sats uint64) *sdkTx.TransactionInput { |
| 205 | + in := &sdkTx.TransactionInput{SourceTXID: nonZeroSourceTXID()} |
| 206 | + in.SetSourceTxOutput(&sdkTx.TransactionOutput{Satoshis: sats, LockingScript: nonDataLockingScript()}) |
| 207 | + return in |
| 208 | + } |
| 209 | + |
| 210 | + tx := &sdkTx.Transaction{ |
| 211 | + Inputs: []*sdkTx.TransactionInput{ |
| 212 | + makeInput(maxSatoshis), |
| 213 | + makeInput(maxSatoshis), |
| 214 | + }, |
| 215 | + } |
| 216 | + |
| 217 | + err := v.checkInputs(tx) |
| 218 | + if err == nil { |
| 219 | + t.Fatal("expected error for total input satoshis above maxSatoshis, got nil") |
| 220 | + } |
| 221 | + if !errors.Is(err, ErrTxInputTotalSatoshisTooHigh) { |
| 222 | + t.Errorf("expected ErrTxInputTotalSatoshisTooHigh, got %v", err) |
| 223 | + } |
| 224 | + if !errors.Is(err, ErrTxInputInvalid) { |
| 225 | + t.Errorf("expected ErrTxInputInvalid wrap, got %v", err) |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +// TestCheckInputs_ManySmallInputsCannotOverflow exercises the per-iteration |
| 230 | +// guard via accumulation across multiple inputs. |
| 231 | +func TestCheckInputs_ManySmallInputsCannotOverflow(t *testing.T) { |
| 232 | + v := NewValidator(nil, nil) |
| 233 | + |
| 234 | + half := uint64(maxSatoshis / 2) |
| 235 | + makeInput := func(sats uint64) *sdkTx.TransactionInput { |
| 236 | + in := &sdkTx.TransactionInput{SourceTXID: nonZeroSourceTXID()} |
| 237 | + in.SetSourceTxOutput(&sdkTx.TransactionOutput{Satoshis: sats, LockingScript: nonDataLockingScript()}) |
| 238 | + return in |
| 239 | + } |
| 240 | + |
| 241 | + tx := &sdkTx.Transaction{ |
| 242 | + Inputs: []*sdkTx.TransactionInput{ |
| 243 | + makeInput(half), |
| 244 | + makeInput(half), |
| 245 | + makeInput(half), |
| 246 | + }, |
| 247 | + } |
| 248 | + |
| 249 | + err := v.checkInputs(tx) |
| 250 | + if err == nil { |
| 251 | + t.Fatal("expected error for cumulative input satoshis above maxSatoshis, got nil") |
| 252 | + } |
| 253 | + if !errors.Is(err, ErrTxInputTotalSatoshisTooHigh) { |
| 254 | + t.Errorf("expected ErrTxInputTotalSatoshisTooHigh, got %v", err) |
| 255 | + } |
| 256 | +} |
| 257 | + |
| 258 | +// TestCheckInputs_ValidPasses confirms a legitimate transaction is still |
| 259 | +// accepted by checkInputs after the overflow guard is added. |
| 260 | +func TestCheckInputs_ValidPasses(t *testing.T) { |
| 261 | + v := NewValidator(nil, nil) |
| 262 | + |
| 263 | + makeInput := func(sats uint64) *sdkTx.TransactionInput { |
| 264 | + in := &sdkTx.TransactionInput{SourceTXID: nonZeroSourceTXID()} |
| 265 | + in.SetSourceTxOutput(&sdkTx.TransactionOutput{Satoshis: sats, LockingScript: nonDataLockingScript()}) |
| 266 | + return in |
| 267 | + } |
| 268 | + |
| 269 | + tx := &sdkTx.Transaction{ |
| 270 | + Inputs: []*sdkTx.TransactionInput{ |
| 271 | + makeInput(1_000), |
| 272 | + makeInput(2_000), |
| 273 | + }, |
| 274 | + } |
| 275 | + |
| 276 | + if err := v.checkInputs(tx); err != nil { |
| 277 | + t.Fatalf("expected no error for valid inputs, got %v", err) |
| 278 | + } |
| 279 | +} |
0 commit comments