Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
69ed943
works
Aug 27, 2025
e11f196
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Aug 27, 2025
9c49270
cleanup
Aug 28, 2025
b34789f
cleanup and tests
Aug 28, 2025
060445c
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Aug 28, 2025
9e3b302
cleanup
Aug 28, 2025
aa12502
undo changes
Aug 28, 2025
4fb977e
undo x2
Aug 28, 2025
12d59ca
I fixed the size of the op queue
Aug 29, 2025
9e4ed2c
zk
Aug 29, 2025
7204197
cleanup, fix tests, docs
Sep 2, 2025
f6335c5
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 2, 2025
516a452
Merge branch 'mm/fix-op-queue' into mm/zk-civc
Sep 2, 2025
c9ac144
ugh
Sep 2, 2025
d002636
hm
Sep 2, 2025
f60efd2
update vks
Sep 2, 2025
ede88d2
fix op relation
Sep 2, 2025
42252d6
fix test
Sep 2, 2025
57f35e2
Merge branch 'mm/fix-op-queue' into mm/zk-civc
Sep 3, 2025
ea9189b
Merge remote-tracking branch 'origin/mm/opcode-in-both-rows' into mm/…
Sep 3, 2025
d9b8f06
docs need to update tests
Sep 3, 2025
c475254
improve tests
Sep 4, 2025
3f8b86c
resolve comments from tests
Sep 4, 2025
d01b957
Merge branch 'mm/fix-op-queue' into mm/zk-civc
Sep 4, 2025
0a4dc83
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 4, 2025
8aab49b
undo unwanted change and fix vk hash
Sep 4, 2025
416bd4c
now remove unwanted files
Sep 4, 2025
8f24def
remove unwanted changes
Sep 4, 2025
f5d445f
undo formating of file
Sep 4, 2025
df9c7a6
confusion
Sep 4, 2025
9d90883
cleanup
Sep 4, 2025
0a3166c
works
Sep 5, 2025
51d4b61
resolve comments
Sep 5, 2025
ce6b67a
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 5, 2025
c7d3e1f
Merge branch 'mm/fix-op-queue' into mm/zk-civc
Sep 5, 2025
f40dd8d
cleanup
Sep 8, 2025
e7b9ec1
cleanup and zk for dummies
Sep 8, 2025
16534d9
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 8, 2025
0c35be6
fix merge and cleanups
Sep 8, 2025
e4f39eb
update vk hash
Sep 8, 2025
62d39b7
fix tests
Sep 8, 2025
a195224
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 8, 2025
8fb3bb8
reflect changes in goblin avm recursive verifier
Sep 8, 2025
a932f66
address review comments
Sep 9, 2025
f2345d8
Merge remote-tracking branch 'origin/merge-train/barretenberg' into m…
Sep 9, 2025
e44b0f0
fix typo
Sep 9, 2025
0131cfc
fix build
Sep 9, 2025
11f1bfa
update pinned hash
Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cd ..
# - Generate a hash for versioning: sha256sum bb-civc-inputs.tar.gz
# - Upload the compressed results: aws s3 cp bb-civc-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-civc-inputs-[hash(0:8)].tar.gz
# Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0
pinned_short_hash="d6f612e1"
pinned_short_hash="7b8a64b3"
pinned_civc_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-civc-inputs-${pinned_short_hash}.tar.gz"

function compress_and_upload {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,36 @@ bool TranslatorCircuitChecker::check(const Builder& circuit)
return check_accumulator_transfer(previous_accumulator, gate + 1);
};

auto check_random_op_code = [&](const Fr op_code, size_t gate) {
if (gate % 2 == 0) {
if (op_code == Fr(0) || op_code == Fr(3) || op_code == Fr(4) || op_code == Fr(8)) {
return report_fail("Opcode should be random value at even gate = ", gate);
}
} else {
if (op_code == Fr(0)) {
return report_fail("Opcode should be 0 at odd gate = ", gate);
}
}
return true;
};

// TODO(https: // github.com/AztecProtocol/barretenberg/issues/1367): Report all failures more explicitly and
// consider making use of relations.

auto in_random_range = [&](size_t i) {
return (i >= 2 * Builder::NUM_NO_OPS_START && i < RESULT_ROW) ||
(i >= circuit.num_gates - (circuit.avm_mode ? 0 : 2 * Builder::NUM_RANDOM_OPS_END) &&
i < circuit.num_gates);
};
for (size_t i = 2; i < circuit.num_gates - 1; i += 2) {

// Get the values of P.x
// Ensure random op is present in expected ranges
Fr op_code = circuit.get_variable(op_wire[i]);
if (in_random_range(i)) {
check_random_op_code(op_code, i);
Fr op_code_next = circuit.get_variable(op_wire[i + 1]);
check_random_op_code(op_code_next, i + 1);
continue;
}

// Current accumulator (updated value)
const std::vector current_accumulator_binary_limbs = {
Expand Down
81 changes: 72 additions & 9 deletions barretenberg/cpp/src/barretenberg/client_ivc/client_ivc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,9 @@ ClientIVC::perform_recursive_verification_and_databus_consistency_checks(
pairing_points.aggregate(nested_pairing_points);
if (is_hiding_kernel) {
pairing_points.aggregate(decider_pairing_points);
// Placeholder for randomness at the end of the hiding circuit (to be handled in subsequent PR)
circuit.queue_ecc_no_op();
circuit.queue_ecc_no_op();
// Add randomness at the end of the hiding kernel (whose ecc ops fall right at the end of the op queue table) to
// ensure the CIVC proof doesn't leak information about the actual content of the op queue
hide_op_queue_content_in_hiding(circuit);
}

return { output_verifier_accumulator, pairing_points, merged_table_commitments };
Expand Down Expand Up @@ -317,9 +317,9 @@ void ClientIVC::complete_kernel_circuit_logic(ClientCircuit& circuit)
0U,
"tail kernel ecc ops table should be empty at this point");
circuit.queue_ecc_no_op();
// Placeholder for randomness at the beginning of tail circuit
circuit.queue_ecc_no_op();
circuit.queue_ecc_no_op();
// Add randomness at the begining of the tail kernel (whose ecc ops fall at the beginning of the op queue table)
// to ensure the CIVC proof doesn't leak information about the actual content of the op queue
hide_op_queue_content_in_tail(circuit);
}
circuit.queue_ecc_eq();

Expand Down Expand Up @@ -524,7 +524,8 @@ void ClientIVC::accumulate(ClientCircuit& circuit, const std::shared_ptr<MegaVer
}

/**
* @brief Add a random operation to the op queue to hide its content in Translator computation.
* @brief Add a *real* operation but with random data to the op queue to avoid information leak in Translator
* computation.
*
* @details Translator circuit builder computes the evaluation at some random challenge x of a batched polynomial
* derived from processing the ultra_op version of op_queue. This result (referred to as accumulated_result in
Expand All @@ -542,11 +543,73 @@ void ClientIVC::hide_op_queue_accumulation_result(ClientCircuit& circuit)
circuit.queue_ecc_eq();
}

/**
* @brief Adds three random ops to the tail kernel.
*
* @note The explanation below does not serve as a proof of zero-knowledge but rather as intuition for why the number
* of random ops and their position in the op queue.
*
* @details The ClientIVC proof is sent to the rollup and so it has to be zero-knowledge. In turn, this implies that
* commitments and evaluations to the op queue, when regarded as 4 polynomials in UltraOp format (op, x_lo_y_hi,
* x_hi_z_1, y_lo_z_2), should not leak information about the actual content of the op queue with provenance from
* circuit operations that have been accumulated in CIVC. Since the op queue is used across several provers,
* randomising these polynomials has to be handled in a special way. Normally, to hide a witness we'd add random
* coefficients at proving time when populating ProverPolynomials. However, due to the consistency checks present
* throughout CIVC, to ensure all components use the same op queue data (Merge and Translator on the entire op queue
* table and Merge and Oink on each subtable), randomness has to be added in a common place, this place naturally
* being ClientIVC. ECCVM is not affected by the concerns above, randomness being added to wires at proving time as per
* usual, because the consistency of ECCVMOps processing and UltraOps processing between Translator and ECCVM is
* achieved via the translation evaluation check and avoiding an information leak there is ensured by
* `ClientIVC::hide_op_queue_accumulation_result()` and SmallSubgroupIPA in ECCVM.
*
* We need each op queue polynomial to have 9 random coefficients (so the op queue needs to contain 6 random ops).
Comment thread
maramihali marked this conversation as resolved.
Outdated
*
* For the last subtable of ecc ops belonging to the hiding kernel, merged via appended to the full op queue, its data
* appears as the ecc_op_wires in the MegaZK proof, wires that are not going to be shifted, so the proof contains,
* for each wire, its commitment and evaluation to the Sumcheck challenge. As at least 3 random coefficients are
* needed in each op queue polynomial, we add 2 random ops to the hiding kernel.
*
* The op queue state previous to the append of the last subtable, is the `left_table` in the merge protocol, so for
* the degree check, we construct its inverse polynomial `left_table_inverse`. The MergeProof will contain the
* commitment to the `left_table_inverse` plus its evaluation at Merge protocol challenge κ. Also for the degree check,
* prover needs to send the evaluation of the `left_table` at κ⁻¹. We need to ensure random coefficients are added to
* one of the kernels as not to affect Apps verification keys so the best choice is to add them to the beginning of the
* tail kernel as to not complicate Translator relations. The above advises that another 4 random coefficients are
* needed in the `left_table` (so, 2 random ops).
*
* Finally, the 4 polynomials representing the full ecc op queue table are committed to (in fact, in both Merge
* protocol and Translator but they are commitments to the same data). `x_lo_y_hi`, `x_hi_z_1` and `x_lo_z_2` are
* shifted polynomials in Translator so the Translator proof will contain their evaluation and evaluation of their
* shifts at the Sumcheck challenge. On top of that, the Shplonk proof sent in the last iteration of Merge also
* ascertains the opening of partially_evaluated_difference = left_table + κ^{shift -1 } * right_table - merged_table
* at κ is 0, so a batched quotient commitment is sent in the Merge proof. In total, for each op queue polynomial (or
* parts of its data), there are 4 commitments and 5 evaluations across the CIVC proof so the sweet spot is 5 random
* ops.
*/
void ClientIVC::hide_op_queue_content_in_tail(ClientCircuit& circuit)
{
circuit.queue_ecc_random_op();
circuit.queue_ecc_random_op();
circuit.queue_ecc_random_op();
}

/**
* @brief Adds two random ops to the hiding kernel.
*
* @details For the last subtable of ecc ops belonging to the hiding kernel, merged via appended to the full op
* queue, its data appears as the ecc_op_wires in the MegaZK proof, wires that are not going to be shifted, so the proof
* containts, for each wire, its commitment and evaluation to the Sumcheck challenge. As at least 3 random coefficients
* are needed in each op queue polynomial, we add 2 random ops. More details in `hide_op_queue_content_in_tail`.
*/
void ClientIVC::hide_op_queue_content_in_hiding(ClientCircuit& circuit)
{
circuit.queue_ecc_random_op();
circuit.queue_ecc_random_op();
}

/**
* @brief Construct a zero-knowledge proof for the hiding circuit, which recursively verifies the last folding,
* merge and decider proof.
*
* @return HonkProof - a ZK Mega proof
*/
HonkProof ClientIVC::construct_mega_proof_for_hiding_kernel(ClientCircuit& circuit)
{
Expand Down
2 changes: 2 additions & 0 deletions barretenberg/cpp/src/barretenberg/client_ivc/client_ivc.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ class ClientIVC {
Proof prove();

static void hide_op_queue_accumulation_result(ClientCircuit& circuit);
static void hide_op_queue_content_in_tail(ClientCircuit& circuit);
static void hide_op_queue_content_in_hiding(ClientCircuit& circuit);
HonkProof construct_mega_proof_for_hiding_kernel(ClientCircuit& circuit);

static bool verify(const Proof& proof, const VerificationKey& vk);
Expand Down
2 changes: 1 addition & 1 deletion barretenberg/cpp/src/barretenberg/goblin/goblin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ void Goblin::prove_eccvm()
void Goblin::prove_translator()
{
BB_BENCH_NAME("Goblin::prove_translator");
TranslatorBuilder translator_builder(translation_batching_challenge_v, evaluation_challenge_x, op_queue);
TranslatorBuilder translator_builder(translation_batching_challenge_v, evaluation_challenge_x, op_queue, avm_mode);
auto translator_key = std::make_shared<TranslatorProvingKey>(translator_builder, commitment_key);
TranslatorProver translator_prover(translator_key, transcript);
goblin_proof.translator_proof = translator_prover.construct_proof();
Expand Down
5 changes: 5 additions & 0 deletions barretenberg/cpp/src/barretenberg/goblin/goblin.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class Goblin {

std::deque<MergeProof> merge_verification_queue; // queue of merge proofs to be verified

// In AVM we only use Goblin for a single circuit (it's recursive verifier) whose proof is not required to be
// zero-knowledge. While Translator will still expect to find random ops at the beginning to ensure the accumulation
// result remains at a fixed row we opt for not adding random ops at the end of the op queue.
bool avm_mode = false;

struct VerificationKey {
std::shared_ptr<ECCVMVerificationKey> eccvm_verification_key = std::make_shared<ECCVMVerificationKey>();
std::shared_ptr<TranslatorVerificationKey> translator_verification_key =
Expand Down
12 changes: 7 additions & 5 deletions barretenberg/cpp/src/barretenberg/goblin/mock_circuits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ class GoblinMockCircuits {
/**
* @brief Add some randomness into the op queue.
*/
static void randomise_op_queue(MegaBuilder& builder)
static void randomise_op_queue(MegaBuilder& builder, size_t num_ops = 0)
Comment thread
maramihali marked this conversation as resolved.
Outdated
{
builder.queue_ecc_random_op();
builder.queue_ecc_random_op();

for (size_t i = 0; i < num_ops; ++i) {
builder.queue_ecc_random_op();
}
}

/**
Expand All @@ -153,6 +155,7 @@ class GoblinMockCircuits {
if (idx == num_circuits - 2) {
// Last circuit appended needs to begin with a no-op for translator to be shiftable
builder.queue_ecc_no_op();
randomise_op_queue(builder, TranslatorCircuitBuilder::NUM_RANDOM_OPS_START);
}
construct_simple_circuit(builder);
goblin.prove_merge();
Expand All @@ -161,8 +164,7 @@ class GoblinMockCircuits {
}
MegaCircuitBuilder builder{ goblin.op_queue };
GoblinMockCircuits::construct_simple_circuit(builder);
builder.queue_ecc_no_op();
builder.queue_ecc_no_op();
randomise_op_queue(builder, TranslatorCircuitBuilder::NUM_RANDOM_OPS_END);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class TranslatorRecursiveTests : public ::testing::Test {
static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); }

// Helper function to add no-ops
static void add_no_ops(std::shared_ptr<bb::ECCOpQueue>& op_queue, size_t count = 1)
static void add_random_ops(std::shared_ptr<bb::ECCOpQueue>& op_queue, size_t count = 1)
Comment thread
maramihali marked this conversation as resolved.
Outdated
{
for (size_t i = 0; i < count; i++) {
op_queue->no_op_ultra_only();
op_queue->random_op_ultra_only();
}
}

Expand All @@ -73,11 +73,12 @@ class TranslatorRecursiveTests : public ::testing::Test {

// Add the same operations to the ECC op queue; the native computation is performed under the hood.
auto op_queue = std::make_shared<bb::ECCOpQueue>();
add_no_ops(op_queue);
op_queue->no_op_ultra_only();
add_random_ops(op_queue, InnerBuilder::NUM_RANDOM_OPS_START);
add_mixed_ops(op_queue, circuit_size_parameter / 2);
op_queue->merge();
add_mixed_ops(op_queue, circuit_size_parameter / 2);
add_no_ops(op_queue, 2);
add_random_ops(op_queue, InnerBuilder::NUM_RANDOM_OPS_END);
op_queue->merge(MergeSettings::APPEND, ECCOpQueue::OP_QUEUE_SIZE - op_queue->get_current_subtable_size());

return InnerBuilder{ batching_challenge_v, evaluation_challenge_x, op_queue };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,24 +486,31 @@ TEST_F(TranslatorRelationCorrectnessTests, Decomposition)
TEST_F(TranslatorRelationCorrectnessTests, NonNative)
{
using Flavor = TranslatorFlavor;
using Builder = Flavor::CircuitBuilder;
using FF = typename Flavor::FF;
using BF = typename Flavor::BF;
using ProverPolynomials = typename Flavor::ProverPolynomials;
using GroupElement = typename Flavor::GroupElement;

constexpr size_t NUM_LIMB_BITS = Flavor::NUM_LIMB_BITS;
constexpr auto mini_circuit_size = TranslatorFlavor::MINI_CIRCUIT_SIZE;
constexpr size_t mini_circuit_size = TranslatorFlavor::MINI_CIRCUIT_SIZE;
constexpr size_t mini_circuit_size_without_masking =
TranslatorFlavor::MINI_CIRCUIT_SIZE - TranslatorFlavor::NUM_MASKED_ROWS_END;

auto& engine = numeric::get_debug_randomness();

auto op_queue = std::make_shared<bb::ECCOpQueue>();
op_queue->no_op_ultra_only();
op_queue->random_op_ultra_only();
op_queue->random_op_ultra_only();
op_queue->random_op_ultra_only();

// Generate random EccOpQueue actions

for (size_t i = 0; i < ((mini_circuit_size >> 1) - 2); i++) {
for (size_t i = 0; i < (mini_circuit_size >> 1) / 2; i++) {
switch (engine.get_random_uint8() & 3) {
case 0:
op_queue->empty_row_for_testing();
op_queue->no_op_ultra_only();
break;
case 1:
op_queue->eq_and_reset();
Expand All @@ -517,6 +524,26 @@ TEST_F(TranslatorRelationCorrectnessTests, NonNative)
}
}
op_queue->merge();
for (size_t i = 0; i < 100; i++) {
switch (engine.get_random_uint8() & 3) {
case 0:
op_queue->no_op_ultra_only();
break;
case 1:
op_queue->eq_and_reset();
break;
case 2:
op_queue->add_accumulate(GroupElement::random_element(&engine));
break;
case 3:
op_queue->mul_accumulate(GroupElement::random_element(&engine), FF::random_element(&engine));
break;
}
}
op_queue->random_op_ultra_only();
op_queue->random_op_ultra_only();
op_queue->merge(MergeSettings::APPEND, ECCOpQueue::OP_QUEUE_SIZE - op_queue->get_current_subtable_size());

const auto batching_challenge_v = BF::random_element(&engine);
const auto evaluation_input_x = BF::random_element(&engine);

Expand Down Expand Up @@ -546,7 +573,9 @@ TEST_F(TranslatorRelationCorrectnessTests, NonNative)
ProverPolynomials prover_polynomials = TranslatorFlavor::ProverPolynomials();

// Copy values of wires used in the non-native field relation from the circuit builder
for (size_t i = 1; i < circuit_builder.get_estimated_num_finalized_gates(); i++) {
for (size_t i = Builder::NUM_NO_OPS_START + Builder::NUM_RANDOM_OPS_START;
i < circuit_builder.num_gates - Builder::NUM_RANDOM_OPS_END;
i++) {
prover_polynomials.op.at(i) = circuit_builder.get_variable(circuit_builder.wires[circuit_builder.OP][i]);
prover_polynomials.p_x_low_limbs.at(i) =
circuit_builder.get_variable(circuit_builder.wires[circuit_builder.P_X_LOW_LIMBS][i]);
Expand Down Expand Up @@ -577,7 +606,7 @@ TEST_F(TranslatorRelationCorrectnessTests, NonNative)
}

// Fill in lagrange odd polynomial
for (size_t i = 2; i < mini_circuit_size; i += 2) {
for (size_t i = Flavor::RESULT_ROW; i < mini_circuit_size_without_masking; i += 2) {
prover_polynomials.lagrange_even_in_minicircuit.at(i) = 1;
prover_polynomials.lagrange_odd_in_minicircuit.at(i + 1) = 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ class TranslatorTests : public ::testing::Test {
static void SetUpTestSuite() { bb::srs::init_file_crs_factory(bb::srs::bb_crs_path()); }

// Helper function to add no-ops
static void add_no_ops(std::shared_ptr<bb::ECCOpQueue>& op_queue, size_t count = 1)
static void add_random_ops(std::shared_ptr<bb::ECCOpQueue>& op_queue, size_t count = 1)
{
for (size_t i = 0; i < count; i++) {
op_queue->no_op_ultra_only();
op_queue->random_op_ultra_only();
}
}

Expand All @@ -51,11 +51,12 @@ class TranslatorTests : public ::testing::Test {

// Add the same operations to the ECC op queue; the native computation is performed under the hood.
auto op_queue = std::make_shared<bb::ECCOpQueue>();
add_no_ops(op_queue);
op_queue->no_op_ultra_only();
add_random_ops(op_queue, CircuitBuilder::NUM_RANDOM_OPS_START);
add_mixed_ops(op_queue, circuit_size_parameter / 2);
op_queue->merge();
add_mixed_ops(op_queue, circuit_size_parameter / 2);
add_no_ops(op_queue, 2);
add_random_ops(op_queue, CircuitBuilder::NUM_RANDOM_OPS_END);
op_queue->merge(MergeSettings::APPEND, ECCOpQueue::OP_QUEUE_SIZE - op_queue->get_current_subtable_size());

return CircuitBuilder{ batching_challenge_v, evaluation_challenge_x, op_queue };
Expand Down Expand Up @@ -140,6 +141,26 @@ TEST_F(TranslatorTests, Basic)
EXPECT_TRUE(verified);
}

TEST_F(TranslatorTests, BasicAvmMode)
{
using Fq = fq;

Fq batching_challenge_v = Fq::random_element();
Fq evaluation_challenge_x = Fq::random_element();

// Add the same operations to the ECC op queue; the native computation is performed under the hood.
auto op_queue = std::make_shared<bb::ECCOpQueue>();
op_queue->no_op_ultra_only();
add_random_ops(op_queue, CircuitBuilder::NUM_RANDOM_OPS_START);
add_mixed_ops(op_queue, 100);
op_queue->merge();
auto circuit_builder = CircuitBuilder{ batching_challenge_v, evaluation_challenge_x, op_queue, true };
Comment thread
maramihali marked this conversation as resolved.
Outdated

EXPECT_TRUE(TranslatorCircuitChecker::check(circuit_builder));
bool verified = prove_and_verify(circuit_builder, evaluation_challenge_x, batching_challenge_v);
EXPECT_TRUE(verified);
}

/**
* @brief Ensure that the fixed VK from the default constructor agrees with those computed manually for an arbitrary
* circuit
Expand Down
Loading