Updated RandomX
This commit is contained in:
parent
39e69c2723
commit
21a56c9cbf
9 changed files with 151 additions and 36 deletions
|
@ -263,19 +263,6 @@ int rxa2_validate_inputs(const argon2_context *context) {
|
||||||
return ARGON2_INCORRECT_PARAMETER;
|
return ARGON2_INCORRECT_PARAMETER;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NULL == context->out) {
|
|
||||||
return ARGON2_OUTPUT_PTR_NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validate output length */
|
|
||||||
if (ARGON2_MIN_OUTLEN > context->outlen) {
|
|
||||||
return ARGON2_OUTPUT_TOO_SHORT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ARGON2_MAX_OUTLEN < context->outlen) {
|
|
||||||
return ARGON2_OUTPUT_TOO_LONG;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validate password (required param) */
|
/* Validate password (required param) */
|
||||||
if (NULL == context->pwd) {
|
if (NULL == context->pwd) {
|
||||||
if (0 != context->pwdlen) {
|
if (0 != context->pwdlen) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ namespace randomx {
|
||||||
return data[dataIndex++];
|
return data[dataIndex++];
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Blake2Generator::getInt32() {
|
uint32_t Blake2Generator::getUInt32() {
|
||||||
checkData(4);
|
checkData(4);
|
||||||
auto ret = load32(&data[dataIndex]);
|
auto ret = load32(&data[dataIndex]);
|
||||||
dataIndex += 4;
|
dataIndex += 4;
|
||||||
|
|
|
@ -36,7 +36,7 @@ namespace randomx {
|
||||||
public:
|
public:
|
||||||
Blake2Generator(const void* seed, size_t seedSize, int nonce = 0);
|
Blake2Generator(const void* seed, size_t seedSize, int nonce = 0);
|
||||||
uint8_t getByte();
|
uint8_t getByte();
|
||||||
uint32_t getInt32();
|
uint32_t getUInt32();
|
||||||
private:
|
private:
|
||||||
void checkData(const size_t);
|
void checkData(const size_t);
|
||||||
|
|
||||||
|
|
|
@ -244,7 +244,7 @@ namespace randomx {
|
||||||
|
|
||||||
if (opcode < RandomX_CurrentConfig.CEIL_IMUL_RCP) {
|
if (opcode < RandomX_CurrentConfig.CEIL_IMUL_RCP) {
|
||||||
uint64_t divisor = instr.getImm32();
|
uint64_t divisor = instr.getImm32();
|
||||||
if (!isPowerOf2(divisor)) {
|
if (!isZeroOrPowerOf2(divisor)) {
|
||||||
auto dst = instr.dst % RegistersCount;
|
auto dst = instr.dst % RegistersCount;
|
||||||
ibc.type = InstructionType::IMUL_R;
|
ibc.type = InstructionType::IMUL_R;
|
||||||
ibc.idst = &nreg->r[dst];
|
ibc.idst = &nreg->r[dst];
|
||||||
|
|
|
@ -137,7 +137,7 @@ namespace randomx {
|
||||||
constexpr int RegisterNeedsDisplacement = 5; //x86 r13 register
|
constexpr int RegisterNeedsDisplacement = 5; //x86 r13 register
|
||||||
constexpr int RegisterNeedsSib = 4; //x86 r12 register
|
constexpr int RegisterNeedsSib = 4; //x86 r12 register
|
||||||
|
|
||||||
inline bool isPowerOf2(uint64_t x) {
|
inline bool isZeroOrPowerOf2(uint64_t x) {
|
||||||
return (x & (x - 1)) == 0;
|
return (x & (x - 1)) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -376,11 +376,131 @@ FORCE_INLINE rx_vec_f128 rx_cvt_packed_int_vec_f128(const void* addr) {
|
||||||
|
|
||||||
#define RANDOMX_DEFAULT_FENV
|
#define RANDOMX_DEFAULT_FENV
|
||||||
|
|
||||||
void rx_reset_float_state();
|
#elif defined(__aarch64__)
|
||||||
|
|
||||||
void rx_set_rounding_mode(uint32_t mode);
|
#include <stdlib.h>
|
||||||
|
#include <arm_neon.h>
|
||||||
|
#include <arm_acle.h>
|
||||||
|
|
||||||
#else //end altivec
|
typedef uint8x16_t rx_vec_i128;
|
||||||
|
typedef float64x2_t rx_vec_f128;
|
||||||
|
|
||||||
|
#define rx_aligned_alloc(size, align) aligned_alloc(align, size)
|
||||||
|
#define rx_aligned_free(a) free(a)
|
||||||
|
|
||||||
|
inline void rx_prefetch_nta(void* ptr) {
|
||||||
|
asm volatile ("prfm pldl1strm, [%0]\n" : : "r" (ptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_load_vec_f128(const double* pd) {
|
||||||
|
return vld1q_f64((const float64_t*)pd);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE void rx_store_vec_f128(double* mem_addr, rx_vec_f128 val) {
|
||||||
|
vst1q_f64((float64_t*)mem_addr, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_swap_vec_f128(rx_vec_f128 a) {
|
||||||
|
float64x2_t temp;
|
||||||
|
temp = vcopyq_laneq_f64(temp, 1, a, 1);
|
||||||
|
a = vcopyq_laneq_f64(a, 1, a, 0);
|
||||||
|
return vcopyq_laneq_f64(a, 0, temp, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_set_vec_f128(uint64_t x1, uint64_t x0) {
|
||||||
|
uint64x2_t temp0 = vdupq_n_u64(x0);
|
||||||
|
uint64x2_t temp1 = vdupq_n_u64(x1);
|
||||||
|
return vreinterpretq_f64_u64(vcopyq_laneq_u64(temp0, 1, temp1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_set1_vec_f128(uint64_t x) {
|
||||||
|
return vreinterpretq_f64_u64(vdupq_n_u64(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
#define rx_add_vec_f128 vaddq_f64
|
||||||
|
#define rx_sub_vec_f128 vsubq_f64
|
||||||
|
#define rx_mul_vec_f128 vmulq_f64
|
||||||
|
#define rx_div_vec_f128 vdivq_f64
|
||||||
|
#define rx_sqrt_vec_f128 vsqrtq_f64
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_xor_vec_f128(rx_vec_f128 a, rx_vec_f128 b) {
|
||||||
|
return vreinterpretq_f64_u8(veorq_u8(vreinterpretq_u8_f64(a), vreinterpretq_u8_f64(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_and_vec_f128(rx_vec_f128 a, rx_vec_f128 b) {
|
||||||
|
return vreinterpretq_f64_u8(vandq_u8(vreinterpretq_u8_f64(a), vreinterpretq_u8_f64(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_or_vec_f128(rx_vec_f128 a, rx_vec_f128 b) {
|
||||||
|
return vreinterpretq_f64_u8(vorrq_u8(vreinterpretq_u8_f64(a), vreinterpretq_u8_f64(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __ARM_FEATURE_CRYPTO
|
||||||
|
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_i128 rx_aesenc_vec_i128(rx_vec_i128 a, rx_vec_i128 key) {
|
||||||
|
const uint8x16_t zero = { 0 };
|
||||||
|
return vaesmcq_u8(vaeseq_u8(a, zero)) ^ key;
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_i128 rx_aesdec_vec_i128(rx_vec_i128 a, rx_vec_i128 key) {
|
||||||
|
const uint8x16_t zero = { 0 };
|
||||||
|
return vaesimcq_u8(vaesdq_u8(a, zero)) ^ key;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define HAVE_AES
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define rx_xor_vec_i128 veorq_u8
|
||||||
|
|
||||||
|
FORCE_INLINE int rx_vec_i128_x(rx_vec_i128 a) {
|
||||||
|
return vgetq_lane_s32(vreinterpretq_s32_u8(a), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE int rx_vec_i128_y(rx_vec_i128 a) {
|
||||||
|
return vgetq_lane_s32(vreinterpretq_s32_u8(a), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE int rx_vec_i128_z(rx_vec_i128 a) {
|
||||||
|
return vgetq_lane_s32(vreinterpretq_s32_u8(a), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE int rx_vec_i128_w(rx_vec_i128 a) {
|
||||||
|
return vgetq_lane_s32(vreinterpretq_s32_u8(a), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_i128 rx_set_int_vec_i128(int _I3, int _I2, int _I1, int _I0) {
|
||||||
|
int32_t data[4];
|
||||||
|
data[0] = _I0;
|
||||||
|
data[1] = _I1;
|
||||||
|
data[2] = _I2;
|
||||||
|
data[3] = _I3;
|
||||||
|
return vreinterpretq_u8_s32(vld1q_s32(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
#define rx_xor_vec_i128 veorq_u8
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_i128 rx_load_vec_i128(const rx_vec_i128* mem_addr) {
|
||||||
|
return vld1q_u8((const uint8_t*)mem_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE void rx_store_vec_i128(rx_vec_i128* mem_addr, rx_vec_i128 val) {
|
||||||
|
vst1q_u8((uint8_t*)mem_addr, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE_INLINE rx_vec_f128 rx_cvt_packed_int_vec_f128(const void* addr) {
|
||||||
|
double lo = unsigned32ToSigned2sCompl(load32((uint8_t*)addr + 0));
|
||||||
|
double hi = unsigned32ToSigned2sCompl(load32((uint8_t*)addr + 4));
|
||||||
|
rx_vec_f128 x;
|
||||||
|
x = vsetq_lane_f64(lo, x, 0);
|
||||||
|
x = vsetq_lane_f64(hi, x, 1);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define RANDOMX_DEFAULT_FENV
|
||||||
|
|
||||||
|
#else //portable fallback
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
@ -487,7 +607,6 @@ FORCE_INLINE rx_vec_f128 rx_set1_vec_f128(uint64_t x) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FORCE_INLINE rx_vec_f128 rx_xor_vec_f128(rx_vec_f128 a, rx_vec_f128 b) {
|
FORCE_INLINE rx_vec_f128 rx_xor_vec_f128(rx_vec_f128 a, rx_vec_f128 b) {
|
||||||
rx_vec_f128 x;
|
rx_vec_f128 x;
|
||||||
x.i.u64[0] = a.i.u64[0] ^ b.i.u64[0];
|
x.i.u64[0] = a.i.u64[0] ^ b.i.u64[0];
|
||||||
|
@ -578,10 +697,6 @@ FORCE_INLINE rx_vec_f128 rx_cvt_packed_int_vec_f128(const void* addr) {
|
||||||
|
|
||||||
#define RANDOMX_DEFAULT_FENV
|
#define RANDOMX_DEFAULT_FENV
|
||||||
|
|
||||||
void rx_reset_float_state();
|
|
||||||
|
|
||||||
void rx_set_rounding_mode(uint32_t mode);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef HAVE_AES
|
#ifndef HAVE_AES
|
||||||
|
@ -598,6 +713,14 @@ FORCE_INLINE rx_vec_i128 rx_aesdec_vec_i128(rx_vec_i128 v, rx_vec_i128 rkey) {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef RANDOMX_DEFAULT_FENV
|
||||||
|
|
||||||
|
void rx_reset_float_state();
|
||||||
|
|
||||||
|
void rx_set_rounding_mode(uint32_t mode);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
double loadDoublePortable(const void* addr);
|
double loadDoublePortable(const void* addr);
|
||||||
uint64_t mulh(uint64_t, uint64_t);
|
uint64_t mulh(uint64_t, uint64_t);
|
||||||
int64_t smulh(int64_t, int64_t);
|
int64_t smulh(int64_t, int64_t);
|
||||||
|
|
|
@ -197,7 +197,7 @@ namespace randomx {
|
||||||
// static const uint8_t* NOPX[] = { NOP1, NOP2, NOP3, NOP4, NOP5, NOP6, NOP7, NOP8 };
|
// static const uint8_t* NOPX[] = { NOP1, NOP2, NOP3, NOP4, NOP5, NOP6, NOP7, NOP8 };
|
||||||
|
|
||||||
size_t JitCompilerX86::getCodeSize() {
|
size_t JitCompilerX86::getCodeSize() {
|
||||||
return codePos - prologueSize;
|
return codePos < prologueSize ? 0 : codePos - prologueSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
JitCompilerX86::JitCompilerX86() {
|
JitCompilerX86::JitCompilerX86() {
|
||||||
|
@ -580,7 +580,7 @@ namespace randomx {
|
||||||
|
|
||||||
void JitCompilerX86::h_IMUL_RCP(Instruction& instr, int i) {
|
void JitCompilerX86::h_IMUL_RCP(Instruction& instr, int i) {
|
||||||
uint64_t divisor = instr.getImm32();
|
uint64_t divisor = instr.getImm32();
|
||||||
if (!isPowerOf2(divisor)) {
|
if (!isZeroOrPowerOf2(divisor)) {
|
||||||
registerUsage[instr.dst] = i;
|
registerUsage[instr.dst] = i;
|
||||||
emit(MOV_RAX_I);
|
emit(MOV_RAX_I);
|
||||||
emit64(randomx_reciprocal_fast(divisor));
|
emit64(randomx_reciprocal_fast(divisor));
|
||||||
|
|
|
@ -44,12 +44,14 @@ RandomX_ConfigurationWownero::RandomX_ConfigurationWownero()
|
||||||
ScratchpadL2_Size = 131072;
|
ScratchpadL2_Size = 131072;
|
||||||
ScratchpadL3_Size = 1048576;
|
ScratchpadL3_Size = 1048576;
|
||||||
|
|
||||||
|
RANDOMX_FREQ_IADD_RS = 25;
|
||||||
RANDOMX_FREQ_IROR_R = 10;
|
RANDOMX_FREQ_IROR_R = 10;
|
||||||
RANDOMX_FREQ_IROL_R = 0;
|
RANDOMX_FREQ_IROL_R = 0;
|
||||||
RANDOMX_FREQ_FSWAP_R = 8;
|
RANDOMX_FREQ_FSWAP_R = 8;
|
||||||
RANDOMX_FREQ_FADD_R = 20;
|
RANDOMX_FREQ_FADD_R = 20;
|
||||||
RANDOMX_FREQ_FSUB_R = 20;
|
RANDOMX_FREQ_FSUB_R = 20;
|
||||||
RANDOMX_FREQ_FMUL_R = 20;
|
RANDOMX_FREQ_FMUL_R = 20;
|
||||||
|
RANDOMX_FREQ_CBRANCH = 16;
|
||||||
|
|
||||||
fillAes4Rx4_Key[0] = rx_set_int_vec_i128(0xcf359e95, 0x141f82b7, 0x7ffbe4a6, 0xf890465d);
|
fillAes4Rx4_Key[0] = rx_set_int_vec_i128(0xcf359e95, 0x141f82b7, 0x7ffbe4a6, 0xf890465d);
|
||||||
fillAes4Rx4_Key[1] = rx_set_int_vec_i128(0x6741ffdc, 0xbd5c5ac3, 0xfee8278a, 0x6a55c450);
|
fillAes4Rx4_Key[1] = rx_set_int_vec_i128(0x6741ffdc, 0xbd5c5ac3, 0xfee8278a, 0x6a55c450);
|
||||||
|
@ -68,6 +70,9 @@ RandomX_ConfigurationLoki::RandomX_ConfigurationLoki()
|
||||||
ArgonSalt = "RandomXL\x12";
|
ArgonSalt = "RandomXL\x12";
|
||||||
ProgramSize = 320;
|
ProgramSize = 320;
|
||||||
ProgramCount = 7;
|
ProgramCount = 7;
|
||||||
|
|
||||||
|
RANDOMX_FREQ_IADD_RS = 25;
|
||||||
|
RANDOMX_FREQ_CBRANCH = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
RandomX_ConfigurationBase::RandomX_ConfigurationBase()
|
RandomX_ConfigurationBase::RandomX_ConfigurationBase()
|
||||||
|
@ -87,7 +92,7 @@ RandomX_ConfigurationBase::RandomX_ConfigurationBase()
|
||||||
, ProgramCount(8)
|
, ProgramCount(8)
|
||||||
, JumpBits(8)
|
, JumpBits(8)
|
||||||
, JumpOffset(8)
|
, JumpOffset(8)
|
||||||
, RANDOMX_FREQ_IADD_RS(25)
|
, RANDOMX_FREQ_IADD_RS(16)
|
||||||
, RANDOMX_FREQ_IADD_M(7)
|
, RANDOMX_FREQ_IADD_M(7)
|
||||||
, RANDOMX_FREQ_ISUB_R(16)
|
, RANDOMX_FREQ_ISUB_R(16)
|
||||||
, RANDOMX_FREQ_ISUB_M(7)
|
, RANDOMX_FREQ_ISUB_M(7)
|
||||||
|
@ -113,7 +118,7 @@ RandomX_ConfigurationBase::RandomX_ConfigurationBase()
|
||||||
, RANDOMX_FREQ_FMUL_R(32)
|
, RANDOMX_FREQ_FMUL_R(32)
|
||||||
, RANDOMX_FREQ_FDIV_M(4)
|
, RANDOMX_FREQ_FDIV_M(4)
|
||||||
, RANDOMX_FREQ_FSQRT_R(6)
|
, RANDOMX_FREQ_FSQRT_R(6)
|
||||||
, RANDOMX_FREQ_CBRANCH(16)
|
, RANDOMX_FREQ_CBRANCH(25)
|
||||||
, RANDOMX_FREQ_CFROUND(1)
|
, RANDOMX_FREQ_CFROUND(1)
|
||||||
, RANDOMX_FREQ_ISTORE(16)
|
, RANDOMX_FREQ_ISTORE(16)
|
||||||
, RANDOMX_FREQ_NOP(0)
|
, RANDOMX_FREQ_NOP(0)
|
||||||
|
|
|
@ -329,7 +329,7 @@ namespace randomx {
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (availableRegisters.size() > 1) {
|
if (availableRegisters.size() > 1) {
|
||||||
index = gen.getInt32() % availableRegisters.size();
|
index = gen.getUInt32() % availableRegisters.size();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
index = 0;
|
index = 0;
|
||||||
|
@ -442,7 +442,7 @@ namespace randomx {
|
||||||
case SuperscalarInstructionType::IADD_C8:
|
case SuperscalarInstructionType::IADD_C8:
|
||||||
case SuperscalarInstructionType::IADD_C9: {
|
case SuperscalarInstructionType::IADD_C9: {
|
||||||
mod_ = 0;
|
mod_ = 0;
|
||||||
imm32_ = gen.getInt32();
|
imm32_ = gen.getUInt32();
|
||||||
opGroup_ = SuperscalarInstructionType::IADD_C7;
|
opGroup_ = SuperscalarInstructionType::IADD_C7;
|
||||||
opGroupPar_ = -1;
|
opGroupPar_ = -1;
|
||||||
} break;
|
} break;
|
||||||
|
@ -451,7 +451,7 @@ namespace randomx {
|
||||||
case SuperscalarInstructionType::IXOR_C8:
|
case SuperscalarInstructionType::IXOR_C8:
|
||||||
case SuperscalarInstructionType::IXOR_C9: {
|
case SuperscalarInstructionType::IXOR_C9: {
|
||||||
mod_ = 0;
|
mod_ = 0;
|
||||||
imm32_ = gen.getInt32();
|
imm32_ = gen.getUInt32();
|
||||||
opGroup_ = SuperscalarInstructionType::IXOR_C7;
|
opGroup_ = SuperscalarInstructionType::IXOR_C7;
|
||||||
opGroupPar_ = -1;
|
opGroupPar_ = -1;
|
||||||
} break;
|
} break;
|
||||||
|
@ -461,7 +461,7 @@ namespace randomx {
|
||||||
mod_ = 0;
|
mod_ = 0;
|
||||||
imm32_ = 0;
|
imm32_ = 0;
|
||||||
opGroup_ = SuperscalarInstructionType::IMULH_R;
|
opGroup_ = SuperscalarInstructionType::IMULH_R;
|
||||||
opGroupPar_ = gen.getInt32();
|
opGroupPar_ = gen.getUInt32();
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case SuperscalarInstructionType::ISMULH_R: {
|
case SuperscalarInstructionType::ISMULH_R: {
|
||||||
|
@ -469,14 +469,14 @@ namespace randomx {
|
||||||
mod_ = 0;
|
mod_ = 0;
|
||||||
imm32_ = 0;
|
imm32_ = 0;
|
||||||
opGroup_ = SuperscalarInstructionType::ISMULH_R;
|
opGroup_ = SuperscalarInstructionType::ISMULH_R;
|
||||||
opGroupPar_ = gen.getInt32();
|
opGroupPar_ = gen.getUInt32();
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
case SuperscalarInstructionType::IMUL_RCP: {
|
case SuperscalarInstructionType::IMUL_RCP: {
|
||||||
mod_ = 0;
|
mod_ = 0;
|
||||||
do {
|
do {
|
||||||
imm32_ = gen.getInt32();
|
imm32_ = gen.getUInt32();
|
||||||
} while ((imm32_ & (imm32_ - 1)) == 0);
|
} while (isZeroOrPowerOf2(imm32_));
|
||||||
opGroup_ = SuperscalarInstructionType::IMUL_RCP;
|
opGroup_ = SuperscalarInstructionType::IMUL_RCP;
|
||||||
opGroupPar_ = -1;
|
opGroupPar_ = -1;
|
||||||
} break;
|
} break;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue