mirror of
https://github.com/logos-storage/plonky2.git
synced 2026-08-07 15:13:34 +00:00
Memory channel for program counter (#717)
This commit is contained in:
parent
c27e40e7bb
commit
084700a7f4
@ -1,3 +1,5 @@
|
|||||||
|
use std::iter;
|
||||||
|
|
||||||
use plonky2::field::extension::Extendable;
|
use plonky2::field::extension::Extendable;
|
||||||
use plonky2::field::types::Field;
|
use plonky2::field::types::Field;
|
||||||
use plonky2::hash::hash_types::RichField;
|
use plonky2::hash::hash_types::RichField;
|
||||||
@ -5,6 +7,7 @@ use plonky2::hash::hash_types::RichField;
|
|||||||
use crate::config::StarkConfig;
|
use crate::config::StarkConfig;
|
||||||
use crate::cpu::cpu_stark;
|
use crate::cpu::cpu_stark;
|
||||||
use crate::cpu::cpu_stark::CpuStark;
|
use crate::cpu::cpu_stark::CpuStark;
|
||||||
|
use crate::cpu::membus::NUM_GP_CHANNELS;
|
||||||
use crate::cross_table_lookup::{CrossTableLookup, TableWithColumns};
|
use crate::cross_table_lookup::{CrossTableLookup, TableWithColumns};
|
||||||
use crate::keccak::keccak_stark;
|
use crate::keccak::keccak_stark;
|
||||||
use crate::keccak::keccak_stark::KeccakStark;
|
use crate::keccak::keccak_stark::KeccakStark;
|
||||||
@ -13,8 +16,8 @@ use crate::keccak_memory::keccak_memory_stark;
|
|||||||
use crate::keccak_memory::keccak_memory_stark::KeccakMemoryStark;
|
use crate::keccak_memory::keccak_memory_stark::KeccakMemoryStark;
|
||||||
use crate::logic;
|
use crate::logic;
|
||||||
use crate::logic::LogicStark;
|
use crate::logic::LogicStark;
|
||||||
|
use crate::memory::memory_stark;
|
||||||
use crate::memory::memory_stark::MemoryStark;
|
use crate::memory::memory_stark::MemoryStark;
|
||||||
use crate::memory::{memory_stark, NUM_CHANNELS};
|
|
||||||
use crate::stark::Stark;
|
use crate::stark::Stark;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -129,11 +132,16 @@ fn ctl_logic<F: Field>() -> CrossTableLookup<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ctl_memory<F: Field>() -> CrossTableLookup<F> {
|
fn ctl_memory<F: Field>() -> CrossTableLookup<F> {
|
||||||
let cpu_memory_ops = (0..NUM_CHANNELS).map(|channel| {
|
let cpu_memory_code_read = TableWithColumns::new(
|
||||||
|
Table::Cpu,
|
||||||
|
cpu_stark::ctl_data_code_memory(),
|
||||||
|
Some(cpu_stark::ctl_filter_code_memory()),
|
||||||
|
);
|
||||||
|
let cpu_memory_gp_ops = (0..NUM_GP_CHANNELS).map(|channel| {
|
||||||
TableWithColumns::new(
|
TableWithColumns::new(
|
||||||
Table::Cpu,
|
Table::Cpu,
|
||||||
cpu_stark::ctl_data_memory(channel),
|
cpu_stark::ctl_data_gp_memory(channel),
|
||||||
Some(cpu_stark::ctl_filter_memory(channel)),
|
Some(cpu_stark::ctl_filter_gp_memory(channel)),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let keccak_memory_reads = (0..KECCAK_WIDTH_BYTES).map(|i| {
|
let keccak_memory_reads = (0..KECCAK_WIDTH_BYTES).map(|i| {
|
||||||
@ -150,7 +158,8 @@ fn ctl_memory<F: Field>() -> CrossTableLookup<F> {
|
|||||||
Some(keccak_memory_stark::ctl_filter()),
|
Some(keccak_memory_stark::ctl_filter()),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let all_lookers = cpu_memory_ops
|
let all_lookers = iter::once(cpu_memory_code_read)
|
||||||
|
.chain(cpu_memory_gp_ops)
|
||||||
.chain(keccak_memory_reads)
|
.chain(keccak_memory_reads)
|
||||||
.chain(keccak_memory_writes)
|
.chain(keccak_memory_writes)
|
||||||
.collect();
|
.collect();
|
||||||
@ -725,6 +734,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore] // Ignoring but not deleting so the test can serve as an API usage example
|
||||||
fn test_all_stark() -> Result<()> {
|
fn test_all_stark() -> Result<()> {
|
||||||
let config = StarkConfig::standard_fast_config();
|
let config = StarkConfig::standard_fast_config();
|
||||||
let (all_stark, proof) = get_proof(&config)?;
|
let (all_stark, proof) = get_proof(&config)?;
|
||||||
@ -732,6 +742,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore] // Ignoring but not deleting so the test can serve as an API usage example
|
||||||
fn test_all_stark_recursive_verifier() -> Result<()> {
|
fn test_all_stark_recursive_verifier() -> Result<()> {
|
||||||
init_logger();
|
init_logger();
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ use std::mem::{size_of, transmute};
|
|||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
use crate::cpu::columns::general::CpuGeneralColumnsView;
|
use crate::cpu::columns::general::CpuGeneralColumnsView;
|
||||||
|
use crate::cpu::membus::NUM_GP_CHANNELS;
|
||||||
use crate::memory;
|
use crate::memory;
|
||||||
use crate::util::{indices_arr, transmute_no_compile_time_size_checks};
|
use crate::util::{indices_arr, transmute_no_compile_time_size_checks};
|
||||||
|
|
||||||
@ -35,6 +36,13 @@ pub struct CpuColumnsView<T: Copy> {
|
|||||||
/// Lets us re-use columns in non-cycle rows.
|
/// Lets us re-use columns in non-cycle rows.
|
||||||
pub is_cpu_cycle: T,
|
pub is_cpu_cycle: T,
|
||||||
|
|
||||||
|
/// If CPU cycle: Current context.
|
||||||
|
// TODO: this is currently unconstrained
|
||||||
|
pub context: T,
|
||||||
|
|
||||||
|
/// If CPU cycle: Context for code memory channel.
|
||||||
|
pub code_context: T,
|
||||||
|
|
||||||
/// If CPU cycle: The program counter for the current instruction.
|
/// If CPU cycle: The program counter for the current instruction.
|
||||||
pub program_counter: T,
|
pub program_counter: T,
|
||||||
|
|
||||||
@ -159,7 +167,7 @@ pub struct CpuColumnsView<T: Copy> {
|
|||||||
pub(crate) general: CpuGeneralColumnsView<T>,
|
pub(crate) general: CpuGeneralColumnsView<T>,
|
||||||
|
|
||||||
pub(crate) clock: T,
|
pub(crate) clock: T,
|
||||||
pub mem_channels: [MemoryChannelView<T>; memory::NUM_CHANNELS],
|
pub mem_channels: [MemoryChannelView<T>; NUM_GP_CHANNELS],
|
||||||
}
|
}
|
||||||
|
|
||||||
// `u8` is guaranteed to have a `size_of` of 1.
|
// `u8` is guaranteed to have a `size_of` of 1.
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use std::borrow::{Borrow, BorrowMut};
|
use std::borrow::{Borrow, BorrowMut};
|
||||||
|
use std::iter::repeat;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -10,10 +11,11 @@ use plonky2::hash::hash_types::RichField;
|
|||||||
use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer};
|
use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer};
|
||||||
use crate::cpu::columns::{CpuColumnsView, COL_MAP, NUM_CPU_COLUMNS};
|
use crate::cpu::columns::{CpuColumnsView, COL_MAP, NUM_CPU_COLUMNS};
|
||||||
use crate::cpu::{
|
use crate::cpu::{
|
||||||
bootstrap_kernel, control_flow, decode, jumps, simple_logic, stack_bounds, syscalls,
|
bootstrap_kernel, control_flow, decode, jumps, membus, simple_logic, stack_bounds, syscalls,
|
||||||
};
|
};
|
||||||
use crate::cross_table_lookup::Column;
|
use crate::cross_table_lookup::Column;
|
||||||
use crate::memory::NUM_CHANNELS;
|
use crate::memory::segments::Segment;
|
||||||
|
use crate::memory::{NUM_CHANNELS, VALUE_LIMBS};
|
||||||
use crate::stark::Stark;
|
use crate::stark::Stark;
|
||||||
use crate::vars::{StarkEvaluationTargets, StarkEvaluationVars};
|
use crate::vars::{StarkEvaluationTargets, StarkEvaluationVars};
|
||||||
|
|
||||||
@ -25,14 +27,13 @@ pub fn ctl_data_keccak<F: Field>() -> Vec<Column<F>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ctl_data_keccak_memory<F: Field>() -> Vec<Column<F>> {
|
pub fn ctl_data_keccak_memory<F: Field>() -> Vec<Column<F>> {
|
||||||
// When executing KECCAK_GENERAL, the memory channels are used as follows:
|
// When executing KECCAK_GENERAL, the GP memory channels are used as follows:
|
||||||
// channel 0: instruction
|
// GP channel 0: stack[-1] = context
|
||||||
// channel 1: stack[-1] = context
|
// GP channel 1: stack[-2] = segment
|
||||||
// channel 2: stack[-2] = segment
|
// GP channel 2: stack[-3] = virtual
|
||||||
// channel 3: stack[-3] = virtual
|
let context = Column::single(COL_MAP.mem_channels[0].value[0]);
|
||||||
let context = Column::single(COL_MAP.mem_channels[1].value[0]);
|
let segment = Column::single(COL_MAP.mem_channels[1].value[0]);
|
||||||
let segment = Column::single(COL_MAP.mem_channels[2].value[0]);
|
let virt = Column::single(COL_MAP.mem_channels[2].value[0]);
|
||||||
let virt = Column::single(COL_MAP.mem_channels[3].value[0]);
|
|
||||||
|
|
||||||
let num_channels = F::from_canonical_usize(NUM_CHANNELS);
|
let num_channels = F::from_canonical_usize(NUM_CHANNELS);
|
||||||
let clock = Column::linear_combination([(COL_MAP.clock, num_channels)]);
|
let clock = Column::linear_combination([(COL_MAP.clock, num_channels)]);
|
||||||
@ -60,29 +61,57 @@ pub fn ctl_filter_logic<F: Field>() -> Column<F> {
|
|||||||
Column::sum([COL_MAP.is_and, COL_MAP.is_or, COL_MAP.is_xor])
|
Column::sum([COL_MAP.is_and, COL_MAP.is_or, COL_MAP.is_xor])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ctl_data_memory<F: Field>(channel: usize) -> Vec<Column<F>> {
|
pub const MEM_CODE_CHANNEL_IDX: usize = 0;
|
||||||
debug_assert!(channel < NUM_CHANNELS);
|
pub const MEM_GP_CHANNELS_IDX_START: usize = MEM_CODE_CHANNEL_IDX + 1;
|
||||||
|
|
||||||
|
/// Make the time/channel column for memory lookups.
|
||||||
|
fn mem_time_and_channel<F: Field>(channel: usize) -> Column<F> {
|
||||||
|
let scalar = F::from_canonical_usize(NUM_CHANNELS);
|
||||||
|
let addend = F::from_canonical_usize(channel);
|
||||||
|
Column::linear_combination_with_constant([(COL_MAP.clock, scalar)], addend)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ctl_data_code_memory<F: Field>() -> Vec<Column<F>> {
|
||||||
|
let mut cols = vec![
|
||||||
|
Column::constant(F::ONE), // is_read
|
||||||
|
Column::single(COL_MAP.code_context), // addr_context
|
||||||
|
Column::constant(F::from_canonical_u64(Segment::Code as u64)), // addr_segment
|
||||||
|
Column::single(COL_MAP.program_counter), // addr_virtual
|
||||||
|
];
|
||||||
|
|
||||||
|
// Low limb of the value matches the opcode bits
|
||||||
|
cols.push(Column::le_bits(COL_MAP.opcode_bits));
|
||||||
|
|
||||||
|
// High limbs of the value are all zero.
|
||||||
|
cols.extend(repeat(Column::constant(F::ZERO)).take(VALUE_LIMBS - 1));
|
||||||
|
|
||||||
|
cols.push(mem_time_and_channel(MEM_CODE_CHANNEL_IDX));
|
||||||
|
|
||||||
|
cols
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ctl_data_gp_memory<F: Field>(channel: usize) -> Vec<Column<F>> {
|
||||||
let channel_map = COL_MAP.mem_channels[channel];
|
let channel_map = COL_MAP.mem_channels[channel];
|
||||||
let mut cols: Vec<Column<F>> = Column::singles([
|
let mut cols: Vec<_> = Column::singles([
|
||||||
channel_map.is_read,
|
channel_map.is_read,
|
||||||
channel_map.addr_context,
|
channel_map.addr_context,
|
||||||
channel_map.addr_segment,
|
channel_map.addr_segment,
|
||||||
channel_map.addr_virtual,
|
channel_map.addr_virtual,
|
||||||
])
|
])
|
||||||
.collect_vec();
|
.collect();
|
||||||
|
|
||||||
cols.extend(Column::singles(channel_map.value));
|
cols.extend(Column::singles(channel_map.value));
|
||||||
|
|
||||||
let scalar = F::from_canonical_usize(NUM_CHANNELS);
|
cols.push(mem_time_and_channel(MEM_GP_CHANNELS_IDX_START + channel));
|
||||||
let addend = F::from_canonical_usize(channel);
|
|
||||||
cols.push(Column::linear_combination_with_constant(
|
|
||||||
[(COL_MAP.clock, scalar)],
|
|
||||||
addend,
|
|
||||||
));
|
|
||||||
|
|
||||||
cols
|
cols
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ctl_filter_memory<F: Field>(channel: usize) -> Column<F> {
|
pub fn ctl_filter_code_memory<F: Field>() -> Column<F> {
|
||||||
|
Column::single(COL_MAP.is_cpu_cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ctl_filter_gp_memory<F: Field>(channel: usize) -> Column<F> {
|
||||||
Column::single(COL_MAP.mem_channels[channel].used)
|
Column::single(COL_MAP.mem_channels[channel].used)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +124,7 @@ impl<F: RichField, const D: usize> CpuStark<F, D> {
|
|||||||
pub fn generate(&self, local_values: &mut [F; NUM_CPU_COLUMNS]) {
|
pub fn generate(&self, local_values: &mut [F; NUM_CPU_COLUMNS]) {
|
||||||
let local_values: &mut CpuColumnsView<_> = local_values.borrow_mut();
|
let local_values: &mut CpuColumnsView<_> = local_values.borrow_mut();
|
||||||
decode::generate(local_values);
|
decode::generate(local_values);
|
||||||
|
membus::generate(local_values);
|
||||||
simple_logic::generate(local_values);
|
simple_logic::generate(local_values);
|
||||||
stack_bounds::generate(local_values); // Must come after `decode`.
|
stack_bounds::generate(local_values); // Must come after `decode`.
|
||||||
}
|
}
|
||||||
@ -117,6 +147,7 @@ impl<F: RichField + Extendable<D>, const D: usize> Stark<F, D> for CpuStark<F, D
|
|||||||
control_flow::eval_packed_generic(local_values, next_values, yield_constr);
|
control_flow::eval_packed_generic(local_values, next_values, yield_constr);
|
||||||
decode::eval_packed_generic(local_values, yield_constr);
|
decode::eval_packed_generic(local_values, yield_constr);
|
||||||
jumps::eval_packed(local_values, next_values, yield_constr);
|
jumps::eval_packed(local_values, next_values, yield_constr);
|
||||||
|
membus::eval_packed(local_values, yield_constr);
|
||||||
simple_logic::eval_packed(local_values, yield_constr);
|
simple_logic::eval_packed(local_values, yield_constr);
|
||||||
stack_bounds::eval_packed(local_values, yield_constr);
|
stack_bounds::eval_packed(local_values, yield_constr);
|
||||||
syscalls::eval_packed(local_values, next_values, yield_constr);
|
syscalls::eval_packed(local_values, next_values, yield_constr);
|
||||||
@ -134,6 +165,7 @@ impl<F: RichField + Extendable<D>, const D: usize> Stark<F, D> for CpuStark<F, D
|
|||||||
control_flow::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
control_flow::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
||||||
decode::eval_ext_circuit(builder, local_values, yield_constr);
|
decode::eval_ext_circuit(builder, local_values, yield_constr);
|
||||||
jumps::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
jumps::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
||||||
|
membus::eval_ext_circuit(builder, local_values, yield_constr);
|
||||||
simple_logic::eval_ext_circuit(builder, local_values, yield_constr);
|
simple_logic::eval_ext_circuit(builder, local_values, yield_constr);
|
||||||
stack_bounds::eval_ext_circuit(builder, local_values, yield_constr);
|
stack_bounds::eval_ext_circuit(builder, local_values, yield_constr);
|
||||||
syscalls::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
syscalls::eval_ext_circuit(builder, local_values, next_values, yield_constr);
|
||||||
|
|||||||
70
evm/src/cpu/membus.rs
Normal file
70
evm/src/cpu/membus.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use plonky2::field::extension::Extendable;
|
||||||
|
use plonky2::field::packed::PackedField;
|
||||||
|
use plonky2::field::types::PrimeField64;
|
||||||
|
use plonky2::hash::hash_types::RichField;
|
||||||
|
use plonky2::iop::ext_target::ExtensionTarget;
|
||||||
|
|
||||||
|
use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer};
|
||||||
|
use crate::cpu::columns::CpuColumnsView;
|
||||||
|
|
||||||
|
/// General-purpose memory channels; they can read and write to all contexts/segments/addresses.
|
||||||
|
pub const NUM_GP_CHANNELS: usize = 4;
|
||||||
|
|
||||||
|
pub mod channel_indices {
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
pub const CODE: usize = 0;
|
||||||
|
pub const GP: Range<usize> = CODE + 1..(CODE + 1) + super::NUM_GP_CHANNELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Total memory channels used by the CPU table. This includes all the `GP_MEM_CHANNELS` as well as
|
||||||
|
/// all special-purpose memory channels.
|
||||||
|
///
|
||||||
|
/// Currently, there is one special-purpose memory channel, which reads the opcode from memory. Its
|
||||||
|
/// limitations are:
|
||||||
|
/// - it is enabled by `is_cpu_cycle`,
|
||||||
|
/// - it always reads and cannot write,
|
||||||
|
/// - the context is derived from the current context and the `is_kernel_mode` flag,
|
||||||
|
/// - the segment is hard-wired to the code segment,
|
||||||
|
/// - the address is `program_counter`,
|
||||||
|
/// - the value must fit in one byte (in the least-significant position) and its eight bits are
|
||||||
|
/// found in `opcode_bits`.
|
||||||
|
/// These limitations save us numerous columns in the CPU table.
|
||||||
|
pub const NUM_CHANNELS: usize = channel_indices::GP.end;
|
||||||
|
|
||||||
|
/// Calculates `lv.stack_len_bounds_aux`. Note that this must be run after decode.
|
||||||
|
pub fn generate<F: PrimeField64>(lv: &mut CpuColumnsView<F>) {
|
||||||
|
let cycle_filter = lv.is_cpu_cycle;
|
||||||
|
if cycle_filter == F::ZERO {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(lv.is_kernel_mode.to_canonical_u64() <= 1);
|
||||||
|
|
||||||
|
// Set `lv.code_context` to 0 if in kernel mode and to `lv.context` if in user mode.
|
||||||
|
lv.code_context = (F::ONE - lv.is_kernel_mode) * lv.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_packed<P: PackedField>(
|
||||||
|
lv: &CpuColumnsView<P>,
|
||||||
|
yield_constr: &mut ConstraintConsumer<P>,
|
||||||
|
) {
|
||||||
|
// Validate `lv.code_context`. It should be 0 if in kernel mode and `lv.context` if in user
|
||||||
|
// mode.
|
||||||
|
yield_constr.constraint(
|
||||||
|
lv.is_cpu_cycle * (lv.code_context - (P::ONES - lv.is_kernel_mode) * lv.context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_ext_circuit<F: RichField + Extendable<D>, const D: usize>(
|
||||||
|
builder: &mut plonky2::plonk::circuit_builder::CircuitBuilder<F, D>,
|
||||||
|
lv: &CpuColumnsView<ExtensionTarget<D>>,
|
||||||
|
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
|
||||||
|
) {
|
||||||
|
// Validate `lv.code_context`. It should be 0 if in kernel mode and `lv.context` if in user
|
||||||
|
// mode.
|
||||||
|
let diff = builder.sub_extension(lv.context, lv.code_context);
|
||||||
|
let constr = builder.mul_sub_extension(lv.is_kernel_mode, lv.context, diff);
|
||||||
|
let filtered_constr = builder.mul_extension(lv.is_cpu_cycle, constr);
|
||||||
|
yield_constr.constraint(builder, filtered_constr);
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ pub mod cpu_stark;
|
|||||||
pub(crate) mod decode;
|
pub(crate) mod decode;
|
||||||
mod jumps;
|
mod jumps;
|
||||||
pub mod kernel;
|
pub mod kernel;
|
||||||
|
pub(crate) mod membus;
|
||||||
mod simple_logic;
|
mod simple_logic;
|
||||||
mod stack_bounds;
|
mod stack_bounds;
|
||||||
mod syscalls;
|
mod syscalls;
|
||||||
|
|||||||
@ -3,5 +3,5 @@ pub mod memory_stark;
|
|||||||
pub mod segments;
|
pub mod segments;
|
||||||
|
|
||||||
// TODO: Move to CPU module, now that channels have been removed from the memory table.
|
// TODO: Move to CPU module, now that channels have been removed from the memory table.
|
||||||
pub(crate) const NUM_CHANNELS: usize = 4;
|
pub(crate) const NUM_CHANNELS: usize = crate::cpu::membus::NUM_CHANNELS;
|
||||||
pub(crate) const VALUE_LIMBS: usize = 8;
|
pub(crate) const VALUE_LIMBS: usize = 8;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user