mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-06 03:18:01 +00:00
Upgradable contracts using set_code function (#10690)
* poc logic * set_code_hash impl, tests, benchmark * Address @xgreenx's comments * Move func defs closer to set_storage * Check if code exists - increment/decrement codehash refcount * Document error for non-existing code hash * Revert unrelated change * Changes due to @athei's review * Fix error handling - comment errors: ReturnCodes - update mock ext implementation - return Error::CodeNotFound when no code for such hash * Emit ContractCodeUpdated when setting new code_hash * Address @athei's comments * Move related defs to the bottom * Minor comment update Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Improve docs * Improve docs * Update frame/contracts/src/wasm/runtime.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Refactor set_code_hash test * Minor change to benchmark Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Minor change to benchmark Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Minor comment refactor Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Address @HCastano's comments * Update seal_set_code_hash comment Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com> * Move set_code_hash after delegate_call * Move function to the bottom * Moved and changed banchmark, added verify block * Bring back previous benchmark * Remove skip_meta for seal_set_code_hash * Bring back skip_meta for seal_set_storage_per_new_kb * Apply weights Co-authored-by: Alexander Theißen <alex.theissen@me.com> Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
83eed8018b
commit
e70ffbf44d
@@ -1971,6 +1971,46 @@ benchmarks! {
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
|
||||
|
||||
seal_set_code_hash {
|
||||
let r in 0 .. API_BENCHMARK_BATCHES;
|
||||
let code_hashes = (0..r * API_BENCHMARK_BATCH_SIZE)
|
||||
.map(|i| {
|
||||
let new_code = WasmModule::<T>::dummy_with_bytes(i);
|
||||
Contracts::<T>::store_code_raw(new_code.code, whitelisted_caller())?;
|
||||
Ok(new_code.hash)
|
||||
})
|
||||
.collect::<Result<Vec<_>, &'static str>>()?;
|
||||
let code_hash_len = code_hashes.get(0).map(|x| x.encode().len()).unwrap_or(0);
|
||||
let code_hashes_bytes = code_hashes.iter().flat_map(|x| x.encode()).collect::<Vec<_>>();
|
||||
let code_hashes_len = code_hashes_bytes.len();
|
||||
|
||||
let code = WasmModule::<T>::from(ModuleDefinition {
|
||||
memory: Some(ImportedMemory::max::<T>()),
|
||||
imported_functions: vec![ImportedFunction {
|
||||
module: "__unstable__",
|
||||
name: "seal_set_code_hash",
|
||||
params: vec![
|
||||
ValueType::I32,
|
||||
],
|
||||
return_type: Some(ValueType::I32),
|
||||
}],
|
||||
data_segments: vec![
|
||||
DataSegment {
|
||||
offset: 0,
|
||||
value: code_hashes_bytes,
|
||||
},
|
||||
],
|
||||
call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![
|
||||
Counter(0, code_hash_len as u32), // code_hash_ptr
|
||||
Regular(Instruction::Call(0)),
|
||||
Regular(Instruction::Drop),
|
||||
])),
|
||||
.. Default::default()
|
||||
});
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
|
||||
|
||||
// We make the assumption that pushing a constant and dropping a value takes roughly
|
||||
// the same amount of time. We follow that `t.load` and `drop` both have the weight
|
||||
// of this benchmark / 2. We need to make this assumption because there is no way
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
use crate::{
|
||||
gas::GasMeter,
|
||||
storage::{self, Storage, WriteOutcome},
|
||||
wasm::{decrement_refcount, increment_refcount},
|
||||
AccountCounter, BalanceOf, CodeHash, Config, ContractInfo, ContractInfoOf, Error, Event,
|
||||
Pallet as Contracts, Schedule,
|
||||
};
|
||||
@@ -239,6 +240,9 @@ pub trait Ext: sealing::Sealed {
|
||||
/// Tests sometimes need to modify and inspect the contract info directly.
|
||||
#[cfg(test)]
|
||||
fn contract_info(&mut self) -> &mut ContractInfo<Self::T>;
|
||||
|
||||
/// Sets new code hash for existing contract.
|
||||
fn set_code_hash(&mut self, hash: CodeHash<Self::T>) -> Result<(), DispatchError>;
|
||||
}
|
||||
|
||||
/// Describes the different functions that can be exported by an [`Executable`].
|
||||
@@ -1182,6 +1186,20 @@ where
|
||||
fn contract_info(&mut self) -> &mut ContractInfo<Self::T> {
|
||||
self.top_frame_mut().contract_info()
|
||||
}
|
||||
|
||||
fn set_code_hash(&mut self, hash: CodeHash<Self::T>) -> Result<(), DispatchError> {
|
||||
increment_refcount::<Self::T>(hash)?;
|
||||
let top_frame = self.top_frame_mut();
|
||||
let prev_hash = top_frame.contract_info().code_hash.clone();
|
||||
decrement_refcount::<Self::T>(prev_hash.clone())?;
|
||||
top_frame.contract_info().code_hash = hash;
|
||||
Contracts::<Self::T>::deposit_event(Event::ContractCodeUpdated {
|
||||
contract: top_frame.account_id.clone(),
|
||||
new_code_hash: hash,
|
||||
old_code_hash: prev_hash,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn deposit_event<T: Config>(topics: Vec<T::Hash>, event: Event<T>) {
|
||||
|
||||
@@ -564,6 +564,16 @@ pub mod pallet {
|
||||
|
||||
/// A code with the specified hash was removed.
|
||||
CodeRemoved { code_hash: T::Hash },
|
||||
|
||||
/// A contract's code was updated.
|
||||
ContractCodeUpdated {
|
||||
/// The contract that has been updated.
|
||||
contract: T::AccountId,
|
||||
/// New code hash that was set for the contract.
|
||||
new_code_hash: T::Hash,
|
||||
/// Previous code hash of the contract.
|
||||
old_code_hash: T::Hash,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
|
||||
@@ -328,6 +328,9 @@ pub struct HostFnWeights<T: Config> {
|
||||
/// Weight per overwritten byte of an item stored with `seal_set_storage`.
|
||||
pub set_storage_per_old_byte: Weight,
|
||||
|
||||
/// Weight of calling `seal_set_code_hash`.
|
||||
pub set_code_hash: Weight,
|
||||
|
||||
/// Weight of calling `seal_clear_storage`.
|
||||
pub clear_storage: Weight,
|
||||
|
||||
@@ -606,6 +609,7 @@ impl<T: Config> Default for HostFnWeights<T> {
|
||||
),
|
||||
debug_message: cost_batched!(seal_debug_message),
|
||||
set_storage: cost_batched!(seal_set_storage),
|
||||
set_code_hash: cost_batched!(seal_set_code_hash),
|
||||
set_storage_per_new_byte: cost_byte_batched!(seal_set_storage_per_new_kb),
|
||||
set_storage_per_old_byte: cost_byte_batched!(seal_set_storage_per_old_kb),
|
||||
clear_storage: cost_batched!(seal_clear_storage),
|
||||
|
||||
@@ -3023,3 +3023,64 @@ fn code_rejected_error_works() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
fn set_code_hash() {
|
||||
let (wasm, code_hash) = compile_module::<Test>("set_code_hash").unwrap();
|
||||
let (new_wasm, new_code_hash) = compile_module::<Test>("new_set_code_hash_contract").unwrap();
|
||||
|
||||
let contract_addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
|
||||
|
||||
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
// Instantiate the 'caller'
|
||||
assert_ok!(Contracts::instantiate_with_code(
|
||||
Origin::signed(ALICE),
|
||||
300_000,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
wasm,
|
||||
vec![],
|
||||
vec![],
|
||||
));
|
||||
// upload new code
|
||||
assert_ok!(Contracts::upload_code(Origin::signed(ALICE), new_wasm.clone(), None));
|
||||
|
||||
// First call sets new code_hash and returns 1
|
||||
let result = Contracts::bare_call(
|
||||
ALICE,
|
||||
contract_addr.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
new_code_hash.as_ref().to_vec(),
|
||||
true,
|
||||
)
|
||||
.result
|
||||
.unwrap();
|
||||
assert_return_code!(result, 1);
|
||||
|
||||
// Second calls new contract code that returns 2
|
||||
let result =
|
||||
Contracts::bare_call(ALICE, contract_addr.clone(), 0, GAS_LIMIT, None, vec![], true)
|
||||
.result
|
||||
.unwrap();
|
||||
assert_return_code!(result, 2);
|
||||
|
||||
// Checking for the last event only
|
||||
assert_eq!(
|
||||
System::events().pop().unwrap(),
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: Event::Contracts(crate::Event::ContractCodeUpdated {
|
||||
contract: contract_addr.clone(),
|
||||
new_code_hash: new_code_hash.clone(),
|
||||
old_code_hash: code_hash.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,6 +117,22 @@ pub fn decrement_refcount<T: Config>(code_hash: CodeHash<T>) -> Result<(), Dispa
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increment the refcount of a code in-storage by one.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::CodeNotFound`] is returned if the specified `code_hash` does not exist.
|
||||
pub fn increment_refcount<T: Config>(code_hash: CodeHash<T>) -> Result<(), DispatchError> {
|
||||
<OwnerInfoOf<T>>::mutate(code_hash, |existing| -> Result<(), DispatchError> {
|
||||
if let Some(info) = existing {
|
||||
info.refcount = info.refcount.saturating_add(1);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::<T>::CodeNotFound.into())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to remove code together with all associated information.
|
||||
pub fn try_remove<T: Config>(origin: &T::AccountId, code_hash: CodeHash<T>) -> DispatchResult {
|
||||
<OwnerInfoOf<T>>::try_mutate_exists(&code_hash, |existing| {
|
||||
|
||||
@@ -26,7 +26,10 @@ mod runtime;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub use self::code_cache::reinstrument;
|
||||
pub use self::runtime::{ReturnCode, Runtime, RuntimeCosts};
|
||||
pub use self::{
|
||||
code_cache::{decrement_refcount, increment_refcount},
|
||||
runtime::{ReturnCode, Runtime, RuntimeCosts},
|
||||
};
|
||||
use crate::{
|
||||
exec::{ExecResult, Executable, ExportedFunction, Ext},
|
||||
gas::GasMeter,
|
||||
@@ -322,6 +325,7 @@ mod tests {
|
||||
gas_meter: GasMeter<Test>,
|
||||
debug_buffer: Vec<u8>,
|
||||
ecdsa_recover: RefCell<Vec<([u8; 65], [u8; 32])>>,
|
||||
code_hashes: Vec<CodeHash<Test>>,
|
||||
}
|
||||
|
||||
/// The call is mocked and just returns this hardcoded value.
|
||||
@@ -332,6 +336,7 @@ mod tests {
|
||||
impl Default for MockExt {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code_hashes: Default::default(),
|
||||
storage: Default::default(),
|
||||
instantiates: Default::default(),
|
||||
terminations: Default::default(),
|
||||
@@ -390,6 +395,10 @@ mod tests {
|
||||
ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(Vec::new()) },
|
||||
))
|
||||
}
|
||||
fn set_code_hash(&mut self, hash: CodeHash<Self::T>) -> Result<(), DispatchError> {
|
||||
self.code_hashes.push(hash);
|
||||
Ok(())
|
||||
}
|
||||
fn transfer(&mut self, to: &AccountIdOf<Self::T>, value: u64) -> Result<(), DispatchError> {
|
||||
self.transfers.push(TransferEntry { to: to.clone(), value });
|
||||
Ok(())
|
||||
@@ -798,6 +807,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
fn contains_storage_works() {
|
||||
const CODE: &str = r#"
|
||||
(module
|
||||
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
|
||||
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
|
||||
(import "__unstable__" "seal_contains_storage" (func $seal_contains_storage (param i32) (result i32)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
;; [0, 4) size of input buffer (32 byte as we copy the key here)
|
||||
(data (i32.const 0) "\20")
|
||||
|
||||
;; [4, 36) input buffer
|
||||
|
||||
;; [36, inf) output buffer
|
||||
|
||||
(func (export "call")
|
||||
;; Receive key
|
||||
(call $seal_input
|
||||
(i32.const 4) ;; Pointer to the input buffer
|
||||
(i32.const 0) ;; Size of the length buffer
|
||||
)
|
||||
|
||||
;; Load the return value into the output buffer
|
||||
(i32.store (i32.const 36)
|
||||
(call $seal_contains_storage
|
||||
(i32.const 4) ;; The pointer to the storage key to fetch
|
||||
)
|
||||
)
|
||||
|
||||
;; Return the contents of the buffer
|
||||
(call $seal_return
|
||||
(i32.const 0) ;; flags
|
||||
(i32.const 36) ;; output buffer ptr
|
||||
(i32.const 4) ;; result is integer (4 bytes)
|
||||
)
|
||||
)
|
||||
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#;
|
||||
|
||||
let mut ext = MockExt::default();
|
||||
|
||||
ext.storage.insert([1u8; 32], vec![42u8]);
|
||||
ext.storage.insert([2u8; 32], vec![]);
|
||||
|
||||
// value does not exist -> sentinel value returned
|
||||
let result = execute(CODE, [3u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), crate::SENTINEL);
|
||||
|
||||
// value did exist -> success
|
||||
let result = execute(CODE, [1u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 1,);
|
||||
|
||||
// value did exist -> success (zero sized type)
|
||||
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0,);
|
||||
}
|
||||
|
||||
const CODE_INSTANTIATE: &str = r#"
|
||||
(module
|
||||
;; seal_instantiate(
|
||||
@@ -2249,67 +2319,6 @@ mod tests {
|
||||
assert_eq!(&result.data.0[4..], &[0u8; 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
fn contains_storage_works() {
|
||||
const CODE: &str = r#"
|
||||
(module
|
||||
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
|
||||
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
|
||||
(import "__unstable__" "seal_contains_storage" (func $seal_contains_storage (param i32) (result i32)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
;; [0, 4) size of input buffer (32 byte as we copy the key here)
|
||||
(data (i32.const 0) "\20")
|
||||
|
||||
;; [4, 36) input buffer
|
||||
|
||||
;; [36, inf) output buffer
|
||||
|
||||
(func (export "call")
|
||||
;; Receive key
|
||||
(call $seal_input
|
||||
(i32.const 4) ;; Pointer to the input buffer
|
||||
(i32.const 0) ;; Size of the length buffer
|
||||
)
|
||||
|
||||
;; Load the return value into the output buffer
|
||||
(i32.store (i32.const 36)
|
||||
(call $seal_contains_storage
|
||||
(i32.const 4) ;; The pointer to the storage key to fetch
|
||||
)
|
||||
)
|
||||
|
||||
;; Return the contents of the buffer
|
||||
(call $seal_return
|
||||
(i32.const 0) ;; flags
|
||||
(i32.const 36) ;; output buffer ptr
|
||||
(i32.const 4) ;; result is integer (4 bytes)
|
||||
)
|
||||
)
|
||||
|
||||
(func (export "deploy"))
|
||||
)
|
||||
"#;
|
||||
|
||||
let mut ext = MockExt::default();
|
||||
|
||||
ext.storage.insert([1u8; 32], vec![42u8]);
|
||||
ext.storage.insert([2u8; 32], vec![]);
|
||||
|
||||
// value does not exist -> sentinel value returned
|
||||
let result = execute(CODE, [3u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), crate::SENTINEL);
|
||||
|
||||
// value did exist -> success
|
||||
let result = execute(CODE, [1u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 1,);
|
||||
|
||||
// value did exist -> success (zero sized type)
|
||||
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
|
||||
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
fn is_contract_works() {
|
||||
@@ -2385,4 +2394,45 @@ mod tests {
|
||||
ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(0u32.encode()) },
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
fn set_code_hash() {
|
||||
const CODE: &str = r#"
|
||||
(module
|
||||
(import "__unstable__" "seal_set_code_hash" (func $seal_set_code_hash (param i32) (result i32)))
|
||||
(import "env" "memory" (memory 1 1))
|
||||
(func $assert (param i32)
|
||||
(block $ok
|
||||
(br_if $ok
|
||||
(get_local 0)
|
||||
)
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
(func (export "call")
|
||||
(local $exit_code i32)
|
||||
(set_local $exit_code
|
||||
(call $seal_set_code_hash (i32.const 0))
|
||||
)
|
||||
(call $assert
|
||||
(i32.eq (get_local $exit_code) (i32.const 0)) ;; ReturnCode::Success
|
||||
)
|
||||
)
|
||||
|
||||
(func (export "deploy"))
|
||||
|
||||
;; Hash of code.
|
||||
(data (i32.const 0)
|
||||
"\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11"
|
||||
"\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11"
|
||||
)
|
||||
)
|
||||
"#;
|
||||
|
||||
let mut mock_ext = MockExt::default();
|
||||
execute(CODE, [0u8; 32].encode(), &mut mock_ext).unwrap();
|
||||
|
||||
assert_eq!(mock_ext.code_hashes.pop().unwrap(), H256::from_slice(&[17u8; 32]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,9 @@ pub enum RuntimeCosts {
|
||||
/// Weight charged for calling into the runtime.
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
CallRuntime(Weight),
|
||||
/// Weight of calling `seal_set_code_hash`
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
SetCodeHash,
|
||||
}
|
||||
|
||||
impl RuntimeCosts {
|
||||
@@ -305,6 +308,8 @@ impl RuntimeCosts {
|
||||
CopyIn(len) => s.return_per_byte.saturating_mul(len.into()),
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
CallRuntime(weight) => weight,
|
||||
#[cfg(feature = "unstable-interface")]
|
||||
SetCodeHash => s.set_code_hash,
|
||||
};
|
||||
RuntimeToken {
|
||||
#[cfg(test)]
|
||||
@@ -1960,4 +1965,41 @@ define_env!(Env, <E: Ext>,
|
||||
Err(_) => Ok(ReturnCode::EcdsaRecoverFailed),
|
||||
}
|
||||
},
|
||||
|
||||
// Replace the contract code at the specified address with new code.
|
||||
//
|
||||
// # Note
|
||||
//
|
||||
// There are a couple of important considerations which must be taken into account when
|
||||
// using this API:
|
||||
//
|
||||
// 1. The storage at the code address will remain untouched. This means that contract developers
|
||||
// must ensure that the storage layout of the new code is compatible with that of the old code.
|
||||
//
|
||||
// 2. Contracts using this API can't be assumed as having deterministic addresses. Said another way,
|
||||
// when using this API you lose the guarantee that an address always identifies a specific code hash.
|
||||
//
|
||||
// 3. If a contract calls into itself after changing its code the new call would use
|
||||
// the new code. However, if the original caller panics after returning from the sub call it
|
||||
// would revert the changes made by `seal_set_code_hash` and the next caller would use
|
||||
// the old code.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - code_hash_ptr: A pointer to the buffer that contains the new code hash.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// `ReturnCode::CodeNotFound`
|
||||
[__unstable__] seal_set_code_hash(ctx, code_hash_ptr: u32) -> ReturnCode => {
|
||||
ctx.charge_gas(RuntimeCosts::SetCodeHash)?;
|
||||
let code_hash: CodeHash<<E as Ext>::T> = ctx.read_sandbox_memory_as(code_hash_ptr)?;
|
||||
match ctx.ext.set_code_hash(code_hash) {
|
||||
Err(err) => {
|
||||
let code = Runtime::<E>::err_into_return_code(err)?;
|
||||
Ok(code)
|
||||
},
|
||||
Ok(()) => Ok(ReturnCode::Success)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user