mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 10:22:23 +00:00
add crypto/poseidon package (#570)
This commit is contained in:
parent
5ffd345358
commit
932d6e371c
5 changed files with 25354 additions and 0 deletions
43
crypto/poseidon/codehash.go
Normal file
43
crypto/poseidon/codehash.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package poseidon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPoseidonChunk = 3
|
||||||
|
const nBytesToFieldElement = 31
|
||||||
|
|
||||||
|
func CodeHash(code []byte) (h common.Hash) {
|
||||||
|
nBytes := int64(len(code))
|
||||||
|
|
||||||
|
// step 1: pad code with 0x0 (STOP) so that len(code) % nBytesToFieldElement == 0
|
||||||
|
// step 2: for every nBytesToFieldElement bytes, convert to Fr, so that we get a Fr array
|
||||||
|
var length = (len(code) + nBytesToFieldElement - 1) / nBytesToFieldElement
|
||||||
|
|
||||||
|
Frs := make([]*big.Int, length)
|
||||||
|
ii := 0
|
||||||
|
|
||||||
|
for ii < length-1 {
|
||||||
|
Frs[ii] = big.NewInt(0)
|
||||||
|
Frs[ii].SetBytes(code[ii*nBytesToFieldElement : (ii+1)*nBytesToFieldElement])
|
||||||
|
ii++
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > 0 {
|
||||||
|
Frs[ii] = big.NewInt(0)
|
||||||
|
bytes := make([]byte, nBytesToFieldElement)
|
||||||
|
copy(bytes, code[ii*nBytesToFieldElement:])
|
||||||
|
Frs[ii].SetBytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// step 3: apply the array onto a sponge process with the current poseidon scheme
|
||||||
|
// (3 Frs permutation and 1 Fr for output, so the throughout is 2 Frs)
|
||||||
|
// step 4: convert final root Fr to u256 (big-endian representation)
|
||||||
|
hash, err := HashWithCap(Frs, defaultPoseidonChunk, nBytes)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}
|
||||||
|
}
|
||||||
|
return common.BigToHash(hash)
|
||||||
|
}
|
||||||
42
crypto/poseidon/codehash_test.go
Normal file
42
crypto/poseidon/codehash_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package poseidon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPoseidonCodeHash(t *testing.T) {
|
||||||
|
// nil
|
||||||
|
got := fmt.Sprintf("%s", CodeHash(nil))
|
||||||
|
want := "0x2098f5fb9e239eab3ceac3f27b81e481dc3124d55ffed523a839ee8446b64864"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// single byte
|
||||||
|
got = fmt.Sprintf("%s", CodeHash([]byte{0}))
|
||||||
|
want = "0x29f94b67ee4e78b2bb08da025f9943c1201a7af025a27600c2dd0a2e71c7cf8b"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = fmt.Sprintf("%s", CodeHash([]byte{1}))
|
||||||
|
want = "0x246d3c06960643350a3e2d587fa16315c381635eb5ac1ac4501e195423dbf78e"
|
||||||
|
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32 bytes
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
for i := range bytes {
|
||||||
|
bytes[i] = 1
|
||||||
|
}
|
||||||
|
got = fmt.Sprintf("%s", CodeHash(bytes))
|
||||||
|
want = "0x0b46d156183dffdbed8e6c6b0af139b95c058e735878ca7f4dca334e0ea8bd20"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, wanted %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
24906
crypto/poseidon/constants.go
Normal file
24906
crypto/poseidon/constants.go
Normal file
File diff suppressed because it is too large
Load diff
185
crypto/poseidon/poseidon.go
Normal file
185
crypto/poseidon/poseidon.go
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
// from github.com/iden3/go-iden3-crypto/ff/poseidon
|
||||||
|
|
||||||
|
package poseidon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/iden3/go-iden3-crypto/ff"
|
||||||
|
"github.com/iden3/go-iden3-crypto/utils"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NROUNDSF = 8 //nolint:golint
|
||||||
|
|
||||||
|
var NROUNDSP = []int{56, 57, 56, 60, 60, 63, 64, 63, 60, 66, 60, 65, 70, 60, 64, 68} //nolint:golint
|
||||||
|
|
||||||
|
func zero() *ff.Element {
|
||||||
|
return ff.NewElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
// exp5 performs x^5 mod p
|
||||||
|
// https://eprint.iacr.org/2019/458.pdf page 8
|
||||||
|
func exp5(a *ff.Element) {
|
||||||
|
a.Exp(*a, big.NewInt(5)) //nolint:gomnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// exp5state perform exp5 for whole state
|
||||||
|
func exp5state(state []*ff.Element) {
|
||||||
|
for i := 0; i < len(state); i++ {
|
||||||
|
exp5(state[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ark computes Add-Round Key, from the paper https://eprint.iacr.org/2019/458.pdf
|
||||||
|
func ark(state []*ff.Element, c []*ff.Element, it int) {
|
||||||
|
for i := 0; i < len(state); i++ {
|
||||||
|
state[i].Add(state[i], c[it+i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mix returns [[matrix]] * [vector]
|
||||||
|
func mix(state []*ff.Element, t int, m [][]*ff.Element) []*ff.Element {
|
||||||
|
mul := zero()
|
||||||
|
newState := make([]*ff.Element, t)
|
||||||
|
for i := 0; i < t; i++ {
|
||||||
|
newState[i] = zero()
|
||||||
|
}
|
||||||
|
for i := 0; i < len(state); i++ {
|
||||||
|
newState[i].SetUint64(0)
|
||||||
|
for j := 0; j < len(state); j++ {
|
||||||
|
mul.Mul(m[j][i], state[j])
|
||||||
|
newState[i].Add(newState[i], mul)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
func permute(state []*ff.Element, t int) []*ff.Element {
|
||||||
|
|
||||||
|
nRoundsF := NROUNDSF
|
||||||
|
nRoundsP := NROUNDSP[t-2]
|
||||||
|
C := c.c[t-2]
|
||||||
|
S := c.s[t-2]
|
||||||
|
M := c.m[t-2]
|
||||||
|
P := c.p[t-2]
|
||||||
|
|
||||||
|
ark(state, C, 0)
|
||||||
|
|
||||||
|
for i := 0; i < nRoundsF/2-1; i++ {
|
||||||
|
exp5state(state)
|
||||||
|
ark(state, C, (i+1)*t)
|
||||||
|
state = mix(state, t, M)
|
||||||
|
}
|
||||||
|
exp5state(state)
|
||||||
|
ark(state, C, (nRoundsF/2)*t)
|
||||||
|
state = mix(state, t, P)
|
||||||
|
|
||||||
|
for i := 0; i < nRoundsP; i++ {
|
||||||
|
exp5(state[0])
|
||||||
|
state[0].Add(state[0], C[(nRoundsF/2+1)*t+i])
|
||||||
|
|
||||||
|
mul := zero()
|
||||||
|
newState0 := zero()
|
||||||
|
for j := 0; j < len(state); j++ {
|
||||||
|
mul.Mul(S[(t*2-1)*i+j], state[j])
|
||||||
|
newState0.Add(newState0, mul)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := 1; k < t; k++ {
|
||||||
|
mul = zero()
|
||||||
|
state[k] = state[k].Add(state[k], mul.Mul(state[0], S[(t*2-1)*i+t+k-1]))
|
||||||
|
}
|
||||||
|
state[0] = newState0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < nRoundsF/2-1; i++ {
|
||||||
|
exp5state(state)
|
||||||
|
ark(state, C, (nRoundsF/2+1)*t+nRoundsP+i*t)
|
||||||
|
state = mix(state, t, M)
|
||||||
|
}
|
||||||
|
exp5state(state)
|
||||||
|
return mix(state, t, M)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for short, use size of inpBI as cap
|
||||||
|
func Hash(inpBI []*big.Int, width int) (*big.Int, error) {
|
||||||
|
return HashWithCap(inpBI, width, int64(len(inpBI)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash using possible sponge specs specified by width (rate from 1 to 15), the size of input is applied as capacity
|
||||||
|
// (notice we do not include width in the capacity )
|
||||||
|
func HashWithCap(inpBI []*big.Int, width int, nBytes int64) (*big.Int, error) {
|
||||||
|
if width < 2 {
|
||||||
|
return nil, fmt.Errorf("width must be ranged from 2 to 16")
|
||||||
|
}
|
||||||
|
if width-2 > len(NROUNDSP) {
|
||||||
|
return nil, fmt.Errorf("invalid inputs width %d, max %d", width, len(NROUNDSP)+1) //nolint:gomnd,lll
|
||||||
|
}
|
||||||
|
|
||||||
|
// capflag = nBytes * 2^64
|
||||||
|
pow64 := big.NewInt(1)
|
||||||
|
pow64.Lsh(pow64, 64)
|
||||||
|
capflag := ff.NewElement().SetBigInt(big.NewInt(nBytes))
|
||||||
|
capflag.Mul(capflag, ff.NewElement().SetBigInt(pow64))
|
||||||
|
|
||||||
|
// initialize the state
|
||||||
|
state := make([]*ff.Element, width)
|
||||||
|
state[0] = capflag
|
||||||
|
for i := 1; i < width; i++ {
|
||||||
|
state[i] = zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
rate := width - 1
|
||||||
|
i := 0
|
||||||
|
// always perform one round of permutation even when input is empty
|
||||||
|
for {
|
||||||
|
// each round absorb at most `rate` elements from `inpBI`
|
||||||
|
for j := 0; j < rate && i < len(inpBI); i, j = i+1, j+1 {
|
||||||
|
state[j+1].Add(state[j+1], ff.NewElement().SetBigInt(inpBI[i]))
|
||||||
|
}
|
||||||
|
state = permute(state, width)
|
||||||
|
if i == len(inpBI) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// squeeze
|
||||||
|
rE := state[0]
|
||||||
|
r := big.NewInt(0)
|
||||||
|
rE.ToBigIntRegular(r)
|
||||||
|
return r, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash computes the Poseidon hash for the given fixed-size inputs, with specified domain field
|
||||||
|
func HashFixedWithDomain(inpBI []*big.Int, domain *big.Int) (*big.Int, error) {
|
||||||
|
t := len(inpBI) + 1
|
||||||
|
if len(inpBI) == 0 || len(inpBI) > len(NROUNDSP) {
|
||||||
|
return nil, fmt.Errorf("invalid inputs length %d, max %d", len(inpBI), len(NROUNDSP)) //nolint:gomnd,lll
|
||||||
|
}
|
||||||
|
if !utils.CheckBigIntArrayInField(inpBI[:]) {
|
||||||
|
return nil, errors.New("inputs values not inside Finite Field")
|
||||||
|
}
|
||||||
|
inp := utils.BigIntArrayToElementArray(inpBI[:])
|
||||||
|
|
||||||
|
state := make([]*ff.Element, t)
|
||||||
|
state[0] = ff.NewElement().SetBigInt(domain)
|
||||||
|
copy(state[1:], inp[:])
|
||||||
|
|
||||||
|
state = permute(state, t)
|
||||||
|
|
||||||
|
rE := state[0]
|
||||||
|
r := big.NewInt(0)
|
||||||
|
rE.ToBigIntRegular(r)
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated HashFixed entry, with domain field is 0
|
||||||
|
func HashFixed(inpBI []*big.Int) (*big.Int, error) {
|
||||||
|
log.Warn("called a deprecated method for poseidon fixed hash", "inputs", inpBI)
|
||||||
|
return HashFixedWithDomain(inpBI, big.NewInt(0))
|
||||||
|
}
|
||||||
178
crypto/poseidon/poseidon_test.go
Normal file
178
crypto/poseidon/poseidon_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
// from github.com/iden3/go-iden3-crypto/ff/poseidon
|
||||||
|
package poseidon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iden3/go-iden3-crypto/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPoseidonHashFixed(t *testing.T) {
|
||||||
|
b0 := big.NewInt(0)
|
||||||
|
b1 := big.NewInt(1)
|
||||||
|
b2 := big.NewInt(2)
|
||||||
|
b3 := big.NewInt(3)
|
||||||
|
b4 := big.NewInt(4)
|
||||||
|
b5 := big.NewInt(5)
|
||||||
|
b6 := big.NewInt(6)
|
||||||
|
b7 := big.NewInt(7)
|
||||||
|
b8 := big.NewInt(8)
|
||||||
|
b9 := big.NewInt(9)
|
||||||
|
b10 := big.NewInt(10)
|
||||||
|
b11 := big.NewInt(11)
|
||||||
|
b12 := big.NewInt(12)
|
||||||
|
b13 := big.NewInt(13)
|
||||||
|
b14 := big.NewInt(14)
|
||||||
|
b15 := big.NewInt(15)
|
||||||
|
b16 := big.NewInt(16)
|
||||||
|
|
||||||
|
h, err := HashFixed([]*big.Int{b1})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"18586133768512220936620570745912940619677854269274689475585506675881198879027",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"7853200120776062878684798364095072458815029376092732009249414926327459813530",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"1018317224307729531995786483840663576608797660851238720571059489595066344487",
|
||||||
|
h.String())
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b0, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"15336558801450556532856248569924170992202208561737609669134139141992924267169",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b3, b4, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"5811595552068139067952687508729883632420015185677766880877743348592482390548",
|
||||||
|
h.String())
|
||||||
|
h, err = HashFixed([]*big.Int{b3, b4, b0, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"12263118664590987767234828103155242843640892839966517009184493198782366909018",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b3, b4, b5, b6})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"20400040500897583745843009878988256314335038853985262692600694741116813247201",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"8354478399926161176778659061636406690034081872658507739535256090879947077494",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b3, b4, b5, b6, b7, b8, b9, b0, b0, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"5540388656744764564518487011617040650780060800286365721923524861648744699539",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b3, b4, b5, b6, b7, b8, b9, b0, b0, b0, b0, b0, b0, b0})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"11882816200654282475720830292386643970958445617880627439994635298904836126497",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixed([]*big.Int{b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"9989051620750914585850546081941653841776809718687451684622678807385399211877",
|
||||||
|
h.String())
|
||||||
|
|
||||||
|
h, err = HashFixedWithDomain([]*big.Int{b1, b2}, big.NewInt(256))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t,
|
||||||
|
"2362370911616048355006851495576377379220050231129891536935411970097789775493",
|
||||||
|
h.String())
|
||||||
|
h_ref, _ := HashFixed([]*big.Int{b1, b2})
|
||||||
|
assert.NotEqual(t, h_ref, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorInputs(t *testing.T) {
|
||||||
|
b0 := big.NewInt(0)
|
||||||
|
b1 := big.NewInt(1)
|
||||||
|
b2 := big.NewInt(2)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = HashFixed([]*big.Int{b1, b2, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0})
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
_, err = HashFixed([]*big.Int{b1, b2, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0})
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "invalid inputs length 17, max 16", err.Error())
|
||||||
|
|
||||||
|
_, err = HashFixed([]*big.Int{b1, b2, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0, b0})
|
||||||
|
require.NotNil(t, err)
|
||||||
|
assert.Equal(t, "invalid inputs length 18, max 16", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputsNotInField(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Very big number, should just return error and not go into endless loop
|
||||||
|
b1 := utils.NewIntFromString("12242166908188651009877250812424843524687801523336557272219921456462821518061999999999999999999999999999999999999999999999999999999999") //nolint:lll
|
||||||
|
_, err = HashFixed([]*big.Int{b1})
|
||||||
|
require.Error(t, err, "inputs values not inside Finite Field")
|
||||||
|
|
||||||
|
// Finite Field const Q, should return error
|
||||||
|
b2 := utils.NewIntFromString("21888242871839275222246405745257275088548364400416034343698204186575808495617") //nolint:lll
|
||||||
|
_, err = HashFixed([]*big.Int{b2})
|
||||||
|
require.Error(t, err, "inputs values not inside Finite Field")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoseidonHash(t *testing.T) {
|
||||||
|
ret, err := Hash(nil, 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash nil for width 3 equal to Hash([0, 0])
|
||||||
|
retRef, err := HashFixed([]*big.Int{big.NewInt(0), big.NewInt(0)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, ret, retRef)
|
||||||
|
|
||||||
|
// hash is different for the cap flag
|
||||||
|
ret1, err := Hash([]*big.Int{big.NewInt(0)}, 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotEqual(t, ret1, retRef)
|
||||||
|
|
||||||
|
ret2, err := HashWithCap([]*big.Int{big.NewInt(0)}, 3, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, ret2, retRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoseidonHash(b *testing.B) {
|
||||||
|
b0 := big.NewInt(0)
|
||||||
|
b1 := utils.NewIntFromString("12242166908188651009877250812424843524687801523336557272219921456462821518061") //nolint:lll
|
||||||
|
b2 := utils.NewIntFromString("12242166908188651009877250812424843524687801523336557272219921456462821518061") //nolint:lll
|
||||||
|
|
||||||
|
bigArray4 := []*big.Int{b1, b2, b0, b0, b0, b0}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
HashFixed(bigArray4) //nolint:errcheck,gosec
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue