|
13 | 13 | #include <gtest/gtest.h> |
14 | 14 |
|
15 | 15 | #include "acir_format.hpp" |
| 16 | +#include "acir_to_constraint_buf.hpp" |
16 | 17 | #include "barretenberg/circuit_checker/circuit_checker.hpp" |
| 18 | +#include "barretenberg/common/get_bytecode.hpp" |
17 | 19 | #include "barretenberg/crypto/poseidon2/poseidon2.hpp" |
18 | 20 | #include "barretenberg/crypto/sha256/sha256.hpp" |
19 | 21 | #include "barretenberg/dsl/acir_format/arithmetic_constraints.hpp" |
|
25 | 27 | #include "barretenberg/dsl/acir_format/test_class.hpp" |
26 | 28 | #include "barretenberg/stdlib_circuit_builders/ultra_circuit_builder.hpp" |
27 | 29 |
|
| 30 | +#include <filesystem> |
| 31 | + |
28 | 32 | using namespace bb; |
29 | 33 | using namespace acir_format; |
30 | 34 |
|
@@ -1447,3 +1451,272 @@ TEST_F(PerBlockGateCountTests, SequentialVsParallelSemanticEquivalence) |
1447 | 1451 | } |
1448 | 1452 | info("Wire-level differences (expected due to gate reordering): ", total_wire_diffs); |
1449 | 1453 | } |
| 1454 | + |
| 1455 | +// Find the acir_tests directory relative to the source tree |
| 1456 | +std::filesystem::path find_acir_tests_dir() |
| 1457 | +{ |
| 1458 | + // Walk up from the build dir to find the repo root |
| 1459 | + // The acir_tests are at barretenberg/acir_tests/acir_tests/ |
| 1460 | + std::filesystem::path candidate = std::filesystem::current_path(); |
| 1461 | + for (int i = 0; i < 10; i++) { |
| 1462 | + auto test_dir = candidate / "barretenberg" / "acir_tests" / "acir_tests"; |
| 1463 | + if (std::filesystem::exists(test_dir)) { |
| 1464 | + return test_dir; |
| 1465 | + } |
| 1466 | + candidate = candidate.parent_path(); |
| 1467 | + } |
| 1468 | + return {}; |
| 1469 | +} |
| 1470 | + |
| 1471 | +// Collect all acir_test directories that have compiled artifacts |
| 1472 | +std::vector<std::filesystem::path> collect_acir_test_programs() |
| 1473 | +{ |
| 1474 | + auto acir_dir = find_acir_tests_dir(); |
| 1475 | + if (acir_dir.empty()) { |
| 1476 | + return {}; |
| 1477 | + } |
| 1478 | + std::vector<std::filesystem::path> programs; |
| 1479 | + for (const auto& entry : std::filesystem::directory_iterator(acir_dir)) { |
| 1480 | + if (!entry.is_directory()) |
| 1481 | + continue; |
| 1482 | + auto program_json = entry.path() / "target" / "program.json"; |
| 1483 | + auto witness_gz = entry.path() / "target" / "witness.gz"; |
| 1484 | + if (std::filesystem::exists(program_json) && std::filesystem::exists(witness_gz)) { |
| 1485 | + programs.push_back(entry.path()); |
| 1486 | + } |
| 1487 | + } |
| 1488 | + std::sort(programs.begin(), programs.end()); |
| 1489 | + return programs; |
| 1490 | +} |
| 1491 | + |
| 1492 | +// Check semantic equivalence between two builders: same block sizes, variable counts, |
| 1493 | +// copy cycle structure, constants, range lists, and lookup tables. |
| 1494 | +// Returns number of failures (0 = all invariants hold). |
| 1495 | +size_t check_semantic_equivalence(const std::string& label, const UltraCircuitBuilder& a, const UltraCircuitBuilder& b) |
| 1496 | +{ |
| 1497 | + size_t failures = 0; |
| 1498 | + |
| 1499 | + // Block sizes must match |
| 1500 | + auto a_blocks = a.blocks.get(); |
| 1501 | + auto b_blocks = b.blocks.get(); |
| 1502 | + for (size_t bl = 0; bl < UltraCircuitBuilder::ExecutionTrace::NUM_BLOCKS; bl++) { |
| 1503 | + if (a_blocks[bl].size() != b_blocks[bl].size()) { |
| 1504 | + info(label, ": block ", bl, " size mismatch: ", a_blocks[bl].size(), " vs ", b_blocks[bl].size()); |
| 1505 | + failures++; |
| 1506 | + } |
| 1507 | + } |
| 1508 | + |
| 1509 | + // Variable count |
| 1510 | + if (a.get_num_variables() != b.get_num_variables()) { |
| 1511 | + info(label, ": variable count mismatch: ", a.get_num_variables(), " vs ", b.get_num_variables()); |
| 1512 | + failures++; |
| 1513 | + } |
| 1514 | + |
| 1515 | + // Copy cycle roots (distinct real_variable_index values) |
| 1516 | + auto count_roots = [](const UltraCircuitBuilder& builder) -> size_t { |
| 1517 | + std::set<uint32_t> roots; |
| 1518 | + for (size_t i = 0; i < builder.get_num_variables(); i++) { |
| 1519 | + roots.insert(builder.real_variable_index[i]); |
| 1520 | + } |
| 1521 | + return roots.size(); |
| 1522 | + }; |
| 1523 | + size_t a_roots = count_roots(a); |
| 1524 | + size_t b_roots = count_roots(b); |
| 1525 | + if (a_roots != b_roots) { |
| 1526 | + info(label, ": copy cycle roots mismatch: ", a_roots, " vs ", b_roots); |
| 1527 | + failures++; |
| 1528 | + } |
| 1529 | + |
| 1530 | + // Constant count |
| 1531 | + if (a.constant_variable_indices.size() != b.constant_variable_indices.size()) { |
| 1532 | + info(label, |
| 1533 | + ": constant count mismatch: ", |
| 1534 | + a.constant_variable_indices.size(), |
| 1535 | + " vs ", |
| 1536 | + b.constant_variable_indices.size()); |
| 1537 | + failures++; |
| 1538 | + } |
| 1539 | + |
| 1540 | + // Range lists: same targets, same variable counts per target |
| 1541 | + if (a.range_lists.size() != b.range_lists.size()) { |
| 1542 | + info(label, ": range list count mismatch: ", a.range_lists.size(), " vs ", b.range_lists.size()); |
| 1543 | + failures++; |
| 1544 | + } |
| 1545 | + for (const auto& [target, a_rl] : a.range_lists) { |
| 1546 | + auto it = b.range_lists.find(target); |
| 1547 | + if (it == b.range_lists.end()) { |
| 1548 | + info(label, ": range target ", target, " missing from second builder"); |
| 1549 | + failures++; |
| 1550 | + } else if (a_rl.variable_indices.size() != it->second.variable_indices.size()) { |
| 1551 | + info(label, |
| 1552 | + ": range target ", |
| 1553 | + target, |
| 1554 | + " variable count mismatch: ", |
| 1555 | + a_rl.variable_indices.size(), |
| 1556 | + " vs ", |
| 1557 | + it->second.variable_indices.size()); |
| 1558 | + failures++; |
| 1559 | + } |
| 1560 | + } |
| 1561 | + |
| 1562 | + // Lookup tables |
| 1563 | + if (a.get_lookup_tables().size() != b.get_lookup_tables().size()) { |
| 1564 | + info(label, |
| 1565 | + ": lookup table count mismatch: ", |
| 1566 | + a.get_lookup_tables().size(), |
| 1567 | + " vs ", |
| 1568 | + b.get_lookup_tables().size()); |
| 1569 | + failures++; |
| 1570 | + } |
| 1571 | + |
| 1572 | + return failures; |
| 1573 | +} |
| 1574 | + |
| 1575 | +// Check bit-identical circuits (every wire, selector, variable, and union-find entry must match). |
| 1576 | +// Returns number of mismatches (0 = identical). |
| 1577 | +size_t check_bit_identical(const std::string& label, UltraCircuitBuilder& a, UltraCircuitBuilder& b) |
| 1578 | +{ |
| 1579 | + size_t mismatches = 0; |
| 1580 | + |
| 1581 | + auto a_blocks = a.blocks.get(); |
| 1582 | + auto b_blocks = b.blocks.get(); |
| 1583 | + for (size_t bl = 0; bl < UltraCircuitBuilder::ExecutionTrace::NUM_BLOCKS; bl++) { |
| 1584 | + if (a_blocks[bl].size() != b_blocks[bl].size()) { |
| 1585 | + info(label, ": block ", bl, " size mismatch: ", a_blocks[bl].size(), " vs ", b_blocks[bl].size()); |
| 1586 | + mismatches++; |
| 1587 | + continue; |
| 1588 | + } |
| 1589 | + size_t count = a_blocks[bl].size(); |
| 1590 | + for (size_t w = 0; w < 4; w++) { |
| 1591 | + for (size_t i = 0; i < count; i++) { |
| 1592 | + if (a_blocks[bl].wires[w][i] != b_blocks[bl].wires[w][i]) |
| 1593 | + mismatches++; |
| 1594 | + } |
| 1595 | + } |
| 1596 | + auto a_sels = a_blocks[bl].get_selectors(); |
| 1597 | + auto b_sels = b_blocks[bl].get_selectors(); |
| 1598 | + for (size_t s = 0; s < a_sels.size(); s++) { |
| 1599 | + for (size_t i = 0; i < count; i++) { |
| 1600 | + if (a_sels[s][i] != b_sels[s][i]) |
| 1601 | + mismatches++; |
| 1602 | + } |
| 1603 | + } |
| 1604 | + } |
| 1605 | + |
| 1606 | + if (a.get_num_variables() != b.get_num_variables()) { |
| 1607 | + info(label, ": variable count mismatch"); |
| 1608 | + mismatches++; |
| 1609 | + } else { |
| 1610 | + for (size_t i = 0; i < a.get_num_variables(); i++) { |
| 1611 | + if (a.real_variable_index[i] != b.real_variable_index[i]) |
| 1612 | + mismatches++; |
| 1613 | + } |
| 1614 | + } |
| 1615 | + |
| 1616 | + return mismatches; |
| 1617 | +} |
| 1618 | + |
| 1619 | +// Parameterized test that runs the 3-way comparison on every acir_test program. |
| 1620 | +class AcirTestParallelEquivalence : public ::testing::TestWithParam<std::filesystem::path> { |
| 1621 | + protected: |
| 1622 | + static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); } |
| 1623 | +}; |
| 1624 | + |
| 1625 | +TEST_P(AcirTestParallelEquivalence, SequentialN1N2) |
| 1626 | +{ |
| 1627 | + auto test_dir = GetParam(); |
| 1628 | + std::string test_name = test_dir.filename().string(); |
| 1629 | + auto program_path = test_dir / "target" / "program.json"; |
| 1630 | + auto witness_path = test_dir / "target" / "witness.gz"; |
| 1631 | + |
| 1632 | + // Load bytecode and witness |
| 1633 | + auto bytecode = get_bytecode(program_path.string()); |
| 1634 | + AcirFormat constraints = circuit_buf_to_acir_format(std::move(bytecode)); |
| 1635 | + auto witness_buf = gunzip(witness_path.string()); |
| 1636 | + WitnessVector witness = witness_buf_to_witness_vector(std::move(witness_buf)); |
| 1637 | + |
| 1638 | + // Print constraint breakdown for diagnostics |
| 1639 | + info(" quad=", |
| 1640 | + constraints.quad_constraints.size(), |
| 1641 | + " big_quad=", |
| 1642 | + constraints.big_quad_constraints.size(), |
| 1643 | + " logic=", |
| 1644 | + constraints.logic_constraints.size(), |
| 1645 | + " range=", |
| 1646 | + constraints.range_constraints.size(), |
| 1647 | + " sha256=", |
| 1648 | + constraints.sha256_compression.size(), |
| 1649 | + " ecdsa_k1=", |
| 1650 | + constraints.ecdsa_k1_constraints.size(), |
| 1651 | + " ecdsa_r1=", |
| 1652 | + constraints.ecdsa_r1_constraints.size(), |
| 1653 | + " poseidon2=", |
| 1654 | + constraints.poseidon2_constraints.size(), |
| 1655 | + " block=", |
| 1656 | + constraints.block_constraints.size(), |
| 1657 | + " msm=", |
| 1658 | + constraints.multi_scalar_mul_constraints.size(), |
| 1659 | + " ec_add=", |
| 1660 | + constraints.ec_add_constraints.size(), |
| 1661 | + " aes128=", |
| 1662 | + constraints.aes128_constraints.size()); |
| 1663 | + |
| 1664 | + // 1. Build sequentially via create_circuit (uses build_constraints) |
| 1665 | + AcirProgram seq_program{ constraints, WitnessVector(witness) }; |
| 1666 | + auto seq_builder = create_circuit<UltraCircuitBuilder>(seq_program, ProgramMetadata{}); |
| 1667 | + |
| 1668 | + // 2. Build via parallel path with N=1 |
| 1669 | + AcirFormat n1_constraints = constraints; |
| 1670 | + UltraCircuitBuilder n1_builder{ WitnessVector(witness), n1_constraints.public_inputs, false }; |
| 1671 | + build_constraints_parallel(n1_builder, n1_constraints, ProgramMetadata{}, /*num_threads=*/1); |
| 1672 | + |
| 1673 | + // 3. Build via parallel path with N=2 |
| 1674 | + AcirFormat n2_constraints = constraints; |
| 1675 | + UltraCircuitBuilder n2_builder{ WitnessVector(witness), n2_constraints.public_inputs, false }; |
| 1676 | + build_constraints_parallel(n2_builder, n2_constraints, ProgramMetadata{}, /*num_threads=*/2); |
| 1677 | + |
| 1678 | + // Print block sizes for all three builders |
| 1679 | + { |
| 1680 | + auto sb = seq_builder.blocks.get(); |
| 1681 | + auto n1b = n1_builder.blocks.get(); |
| 1682 | + auto n2b = n2_builder.blocks.get(); |
| 1683 | + for (size_t bl = 0; bl < UltraCircuitBuilder::ExecutionTrace::NUM_BLOCKS; bl++) { |
| 1684 | + if (sb[bl].size() > 0 || n1b[bl].size() > 0 || n2b[bl].size() > 0) { |
| 1685 | + info(" block ", bl, ": seq=", sb[bl].size(), " n1=", n1b[bl].size(), " n2=", n2b[bl].size()); |
| 1686 | + } |
| 1687 | + } |
| 1688 | + info(" vars: seq=", |
| 1689 | + seq_builder.get_num_variables(), |
| 1690 | + " n1=", |
| 1691 | + n1_builder.get_num_variables(), |
| 1692 | + " n2=", |
| 1693 | + n2_builder.get_num_variables()); |
| 1694 | + } |
| 1695 | + |
| 1696 | + // All three must pass circuit checker |
| 1697 | + bool seq_ok = CircuitChecker::check(seq_builder); |
| 1698 | + bool n1_ok = CircuitChecker::check(n1_builder); |
| 1699 | + bool n2_ok = CircuitChecker::check(n2_builder); |
| 1700 | + EXPECT_TRUE(seq_ok) << test_name << ": sequential CircuitChecker failed"; |
| 1701 | + EXPECT_TRUE(n1_ok) << test_name << ": N=1 CircuitChecker failed"; |
| 1702 | + EXPECT_TRUE(n2_ok) << test_name << ": N=2 CircuitChecker failed"; |
| 1703 | + |
| 1704 | + // Sequential vs N=1: semantic equivalence (same constraints, different order) |
| 1705 | + size_t seq_n1_failures = check_semantic_equivalence(test_name + " seq-vs-n1", seq_builder, n1_builder); |
| 1706 | + EXPECT_EQ(seq_n1_failures, 0) << test_name << ": sequential vs N=1 semantic equivalence failed"; |
| 1707 | + |
| 1708 | + // Sequential vs N=2: semantic equivalence |
| 1709 | + size_t seq_n2_failures = check_semantic_equivalence(test_name + " seq-vs-n2", seq_builder, n2_builder); |
| 1710 | + EXPECT_EQ(seq_n2_failures, 0) << test_name << ": sequential vs N=2 semantic equivalence failed"; |
| 1711 | + |
| 1712 | + // N=1 vs N=2: must be bit-identical |
| 1713 | + size_t n1_n2_mismatches = check_bit_identical(test_name + " n1-vs-n2", n1_builder, n2_builder); |
| 1714 | + EXPECT_EQ(n1_n2_mismatches, 0) << test_name << ": N=1 vs N=2 bit-identical check failed"; |
| 1715 | +} |
| 1716 | + |
| 1717 | +INSTANTIATE_TEST_SUITE_P(AcirTests, |
| 1718 | + AcirTestParallelEquivalence, |
| 1719 | + ::testing::ValuesIn(collect_acir_test_programs()), |
| 1720 | + [](const ::testing::TestParamInfo<std::filesystem::path>& info) { |
| 1721 | + return info.param.filename().string(); |
| 1722 | + }); |
0 commit comments