|
1 | 1 | package bump |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
| 6 | + "encoding/binary" |
5 | 7 | "net/http" |
6 | 8 | "net/http/httptest" |
7 | 9 | "strconv" |
@@ -346,3 +348,180 @@ func TestFetchBlockDataForBUMP_ZeroCapUsesDefault(t *testing.T) { |
346 | 348 | t.Fatalf("negative cap should select the default and succeed, got: %v", err) |
347 | 349 | } |
348 | 350 | } |
| 351 | + |
| 352 | +// --- Untrusted-varint allocation tests (F-008) --------------------------- |
| 353 | + |
| 354 | +// blockBytesWithSubtreeCountVarint builds a binary block payload with the |
| 355 | +// supplied subtreeCount written verbatim as a 9-byte 0xFF varint. The body |
| 356 | +// itself is short, so a parser that allocates based on the varint will trip |
| 357 | +// long before it tries to fill the buffer. |
| 358 | +func blockBytesWithSubtreeCountVarint(t *testing.T, subtreeCount uint64) []byte { |
| 359 | + t.Helper() |
| 360 | + var buf bytes.Buffer |
| 361 | + buf.Write(make([]byte, 80)) // header (zeroed; merkle-root only needs 32 bytes) |
| 362 | + buf.WriteByte(0x00) // txCount = 0 |
| 363 | + buf.WriteByte(0x00) // sizeBytes = 0 |
| 364 | + // subtreeCount as 0xFF + uint64 LE — the largest VarInt encoding form, |
| 365 | + // so we can dial in any uint64 value the test wants. |
| 366 | + buf.WriteByte(0xff) |
| 367 | + var le [8]byte |
| 368 | + binary.LittleEndian.PutUint64(le[:], subtreeCount) |
| 369 | + buf.Write(le[:]) |
| 370 | + return buf.Bytes() |
| 371 | +} |
| 372 | + |
| 373 | +// TestFetchBlockDataForBUMP_RejectsHugeSubtreeCount verifies that a varint |
| 374 | +// claiming far more subtrees than maxSubtreeCount is rejected without |
| 375 | +// attempting a giant preallocation. We run the test through the public |
| 376 | +// fetcher to also exercise the body-cap and HTTP plumbing. |
| 377 | +func TestFetchBlockDataForBUMP_RejectsHugeSubtreeCount(t *testing.T) { |
| 378 | + body := blockBytesWithSubtreeCountVarint(t, 1<<60) // ~1.15 quintillion |
| 379 | + |
| 380 | + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 381 | + w.WriteHeader(http.StatusOK) |
| 382 | + _, _ = w.Write(body) |
| 383 | + })) |
| 384 | + t.Cleanup(srv.Close) |
| 385 | + |
| 386 | + _, _, _, err := FetchBlockDataForBUMP( |
| 387 | + context.Background(), |
| 388 | + []string{srv.URL}, |
| 389 | + "deadbeef", |
| 390 | + zap.NewNop(), |
| 391 | + ) |
| 392 | + if err == nil { |
| 393 | + t.Fatal("expected error for oversized subtree count varint") |
| 394 | + } |
| 395 | + if !strings.Contains(err.Error(), "subtree count") { |
| 396 | + t.Errorf("expected error mentioning subtree count, got: %v", err) |
| 397 | + } |
| 398 | +} |
| 399 | + |
| 400 | +// TestParseBlockBinary_RejectsSubtreeCountAboveBodyCapacity verifies that a |
| 401 | +// subtreeCount which is below maxSubtreeCount but still cannot fit in the |
| 402 | +// remaining body bytes is rejected before the make() call. This catches |
| 403 | +// "plausible-but-impossible" counts that would otherwise allocate hundreds |
| 404 | +// of MiB for a body only kilobytes long. |
| 405 | +func TestParseBlockBinary_RejectsSubtreeCountAboveBodyCapacity(t *testing.T) { |
| 406 | + // 1,000,000 < maxSubtreeCount (10,000,000) so the absolute cap passes, |
| 407 | + // but the body has zero bytes after the varint, so the body-capacity |
| 408 | + // check must reject. |
| 409 | + body := blockBytesWithSubtreeCountVarint(t, 1_000_000) |
| 410 | + _, _, _, err := parseBlockBinary(body) |
| 411 | + if err == nil { |
| 412 | + t.Fatal("expected error for subtree count exceeding body capacity") |
| 413 | + } |
| 414 | + if !strings.Contains(err.Error(), "remaining body capacity") { |
| 415 | + t.Errorf("expected body-capacity error, got: %v", err) |
| 416 | + } |
| 417 | +} |
| 418 | + |
| 419 | +// TestParseBlockBinary_AcceptsZeroSubtreeCount keeps the boundary case |
| 420 | +// covered: an empty subtree list must continue to parse successfully and |
| 421 | +// return zero hashes. |
| 422 | +func TestParseBlockBinary_AcceptsZeroSubtreeCount(t *testing.T) { |
| 423 | + body := minimalValidBlockBytes(t) |
| 424 | + hashes, _, root, err := parseBlockBinary(body) |
| 425 | + if err != nil { |
| 426 | + t.Fatalf("expected success for zero subtree count, got: %v", err) |
| 427 | + } |
| 428 | + if len(hashes) != 0 { |
| 429 | + t.Errorf("expected 0 hashes, got %d", len(hashes)) |
| 430 | + } |
| 431 | + if root == nil { |
| 432 | + t.Errorf("expected non-nil header merkle root") |
| 433 | + } |
| 434 | +} |
| 435 | + |
| 436 | +// TestParseBlockBinary_RejectsCoinbaseBUMPLengthAboveBodyCapacity covers the |
| 437 | +// sibling unbounded allocation: cbBUMPLen is a varint and was previously |
| 438 | +// fed straight into make([]byte, ...). We construct a payload with a valid |
| 439 | +// header + zero subtree hashes + minimal coinbase tx, then a bogus 2^60 |
| 440 | +// cbBUMPLen, and confirm the parser short-circuits to "no coinbase BUMP" |
| 441 | +// instead of allocating an exabyte of memory. |
| 442 | +func TestParseBlockBinary_RejectsCoinbaseBUMPLengthAboveBodyCapacity(t *testing.T) { |
| 443 | + var buf bytes.Buffer |
| 444 | + buf.Write(make([]byte, 80)) // header |
| 445 | + buf.WriteByte(0x00) // txCount = 0 |
| 446 | + buf.WriteByte(0x00) // sizeBytes = 0 |
| 447 | + buf.WriteByte(0x00) // subtreeCount = 0 |
| 448 | + // Minimal-ish coinbase tx: version (4) | inCount=1 | prev hash (32) | |
| 449 | + // prev index (4) | scriptLen=0 | sequence (4) | outCount=0 | locktime (4). |
| 450 | + // The bsv-sdk tx parser is happy with this skeleton even though it would |
| 451 | + // be rejected by consensus — we only need txBytesUsed to advance. |
| 452 | + tx := make([]byte, 0, 64) |
| 453 | + tx = append(tx, 0x01, 0x00, 0x00, 0x00) // version |
| 454 | + tx = append(tx, 0x01) // inCount = 1 |
| 455 | + tx = append(tx, make([]byte, 32)...) // prev hash |
| 456 | + tx = append(tx, 0xff, 0xff, 0xff, 0xff) // prev index |
| 457 | + tx = append(tx, 0x00) // scriptLen = 0 |
| 458 | + tx = append(tx, 0xff, 0xff, 0xff, 0xff) // sequence |
| 459 | + tx = append(tx, 0x00) // outCount = 0 |
| 460 | + tx = append(tx, 0x00, 0x00, 0x00, 0x00) // locktime |
| 461 | + buf.Write(tx) |
| 462 | + buf.WriteByte(0x00) // blockHeight varint = 0 |
| 463 | + // cbBUMPLen as a 0xFF varint with a wildly oversized value. |
| 464 | + buf.WriteByte(0xff) |
| 465 | + var le [8]byte |
| 466 | + binary.LittleEndian.PutUint64(le[:], 1<<60) |
| 467 | + buf.Write(le[:]) |
| 468 | + |
| 469 | + hashes, cb, root, err := parseBlockBinary(buf.Bytes()) |
| 470 | + if err != nil { |
| 471 | + t.Fatalf("parser should not error on bogus cbBUMPLen, got: %v", err) |
| 472 | + } |
| 473 | + if cb != nil { |
| 474 | + t.Errorf("expected nil coinbase BUMP for oversize cbBUMPLen, got %d bytes", len(cb)) |
| 475 | + } |
| 476 | + if len(hashes) != 0 { |
| 477 | + t.Errorf("expected 0 hashes, got %d", len(hashes)) |
| 478 | + } |
| 479 | + if root == nil { |
| 480 | + t.Errorf("expected non-nil header merkle root") |
| 481 | + } |
| 482 | +} |
| 483 | + |
| 484 | +// TestParseBlockBinary_RejectsHugeTxCount verifies that a txCount varint |
| 485 | +// claiming far more transactions than maxTxCount is rejected before the |
| 486 | +// parser advances. txCount is not currently used for allocation, but bounding |
| 487 | +// it is defense-in-depth against future refactors that begin to use the |
| 488 | +// value and surfaces bogus payloads earlier. |
| 489 | +func TestParseBlockBinary_RejectsHugeTxCount(t *testing.T) { |
| 490 | + var buf bytes.Buffer |
| 491 | + buf.Write(make([]byte, 80)) // header (zeroed) |
| 492 | + // txCount as 0xFF + uint64 LE — the largest VarInt encoding form. |
| 493 | + buf.WriteByte(0xff) |
| 494 | + var le [8]byte |
| 495 | + binary.LittleEndian.PutUint64(le[:], 1<<60) |
| 496 | + buf.Write(le[:]) |
| 497 | + |
| 498 | + _, _, _, err := parseBlockBinary(buf.Bytes()) |
| 499 | + if err == nil { |
| 500 | + t.Fatal("expected error for oversized tx count varint") |
| 501 | + } |
| 502 | + if !strings.Contains(err.Error(), "tx count") { |
| 503 | + t.Errorf("expected error mentioning tx count, got: %v", err) |
| 504 | + } |
| 505 | +} |
| 506 | + |
| 507 | +// TestParseBlockBinary_RejectsHugeSizeBytes verifies that a sizeBytes varint |
| 508 | +// claiming a block size beyond maxBlockSizeBytes is rejected. Same rationale |
| 509 | +// as TestParseBlockBinary_RejectsHugeTxCount — defense-in-depth. |
| 510 | +func TestParseBlockBinary_RejectsHugeSizeBytes(t *testing.T) { |
| 511 | + var buf bytes.Buffer |
| 512 | + buf.Write(make([]byte, 80)) // header (zeroed) |
| 513 | + buf.WriteByte(0x00) // txCount = 0 |
| 514 | + // sizeBytes as 0xFF + uint64 LE — the largest VarInt encoding form. |
| 515 | + buf.WriteByte(0xff) |
| 516 | + var le [8]byte |
| 517 | + binary.LittleEndian.PutUint64(le[:], 1<<60) |
| 518 | + buf.Write(le[:]) |
| 519 | + |
| 520 | + _, _, _, err := parseBlockBinary(buf.Bytes()) |
| 521 | + if err == nil { |
| 522 | + t.Fatal("expected error for oversized size bytes varint") |
| 523 | + } |
| 524 | + if !strings.Contains(err.Error(), "block size") { |
| 525 | + t.Errorf("expected error mentioning block size, got: %v", err) |
| 526 | + } |
| 527 | +} |
0 commit comments