Compare commits
5 commits
5fafa6b529
...
99e30759b4
Author | SHA1 | Date | |
---|---|---|---|
DataHoarder | 99e30759b4 | ||
33d7e0c11a | |||
e4edf2338e | |||
91b2ed32a5 | |||
d245705108 |
|
@ -2,3 +2,8 @@ Monero Utilities
|
||||||
================
|
================
|
||||||
|
|
||||||
These are a set of utilities for working with Monero.
|
These are a set of utilities for working with Monero.
|
||||||
|
|
||||||
|
Bitdevs Presentation
|
||||||
|
====================
|
||||||
|
|
||||||
|
Presentation and appendices can be found in "Ring Signatures.pdf"
|
BIN
Ring Signatures.pdf
Normal file
BIN
Ring Signatures.pdf
Normal file
Binary file not shown.
7
go.mod
Normal file
7
go.mod
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module git.gammaspectra.live/P2Pool/moneroutil
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
12
go.sum
Normal file
12
go.sum
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||||
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
||||||
|
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
@ -15,3 +15,18 @@ func HexToHash(h string) (result Hash) {
|
||||||
copy(result[:], byteSlice)
|
copy(result[:], byteSlice)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RandomPubKey takes a random scalar, interprets it as a point on the curve
|
||||||
|
// and then multiplies by 8 to make it a point in the Group
|
||||||
|
func RandomPubKey() (result *Key) {
|
||||||
|
result = new(Key)
|
||||||
|
p3 := new(ExtendedGroupElement)
|
||||||
|
var p1 ProjectiveGroupElement
|
||||||
|
var p2 CompletedGroupElement
|
||||||
|
h := RandomScalar()
|
||||||
|
p1.FromBytes(h)
|
||||||
|
GeMul8(&p2, &p1)
|
||||||
|
p2.ToExtended(p3)
|
||||||
|
p3.ToBytes(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package moneroutil
|
package moneroutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ebfe/keccak"
|
"golang.org/x/crypto/sha3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,7 +13,7 @@ type Hash [HashLength]byte
|
||||||
type Checksum [ChecksumLength]byte
|
type Checksum [ChecksumLength]byte
|
||||||
|
|
||||||
func Keccak256(data ...[]byte) (result Hash) {
|
func Keccak256(data ...[]byte) (result Hash) {
|
||||||
h := keccak.New256()
|
h := sha3.NewLegacyKeccak256()
|
||||||
for _, b := range data {
|
for _, b := range data {
|
||||||
h.Write(b)
|
h.Write(b)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func GetChecksum(data ...[]byte) (result Checksum) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Keccak512(data ...[]byte) (result Hash) {
|
func Keccak512(data ...[]byte) (result Hash) {
|
||||||
h := keccak.New512()
|
h := sha3.NewLegacyKeccak512()
|
||||||
for _, b := range data {
|
for _, b := range data {
|
||||||
h.Write(b)
|
h.Write(b)
|
||||||
}
|
}
|
||||||
|
|
10
key.go
10
key.go
|
@ -2,6 +2,7 @@ package moneroutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -55,3 +56,12 @@ func NewKeyPair() (privKey *Key, pubKey *Key) {
|
||||||
pubKey = privKey.PubKey()
|
pubKey = privKey.PubKey()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseKey(buf io.Reader) (result Key, err error) {
|
||||||
|
key := make([]byte, KeyLength)
|
||||||
|
if _, err = buf.Read(key); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy(result[:], key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
69
ringct.go
69
ringct.go
|
@ -11,20 +11,6 @@ const (
|
||||||
RCTTypeSimple
|
RCTTypeSimple
|
||||||
)
|
)
|
||||||
|
|
||||||
// V = Vector, M = Matrix
|
|
||||||
type KeyV []Key
|
|
||||||
type KeyM []KeyV
|
|
||||||
|
|
||||||
// Confidential Transaction Keys
|
|
||||||
type CtKey struct {
|
|
||||||
destination Key
|
|
||||||
mask Key
|
|
||||||
}
|
|
||||||
|
|
||||||
// V = Vector, M = Matrix
|
|
||||||
type CtKeyV []CtKey
|
|
||||||
type CtKeyM []CtKeyV
|
|
||||||
|
|
||||||
// Pedersen Commitment is generated from this struct
|
// Pedersen Commitment is generated from this struct
|
||||||
// C = aG + bH where a = mask and b = amount
|
// C = aG + bH where a = mask and b = amount
|
||||||
// senderPk is the one-time public key for ECDH exchange
|
// senderPk is the one-time public key for ECDH exchange
|
||||||
|
@ -34,6 +20,7 @@ type ecdhTuple struct {
|
||||||
senderPk Key
|
senderPk Key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Range proof commitments
|
||||||
type Key64 [64]Key
|
type Key64 [64]Key
|
||||||
|
|
||||||
// Borromean Signature
|
// Borromean Signature
|
||||||
|
@ -44,10 +31,10 @@ type BoroSig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MLSAG (Multilayered Linkable Spontaneous Anonymous Group) Signature
|
// MLSAG (Multilayered Linkable Spontaneous Anonymous Group) Signature
|
||||||
type MgSig struct {
|
type MlsagSig struct {
|
||||||
ss KeyM
|
ss [][]Key
|
||||||
cc Key
|
cc Key
|
||||||
ii KeyV
|
ii []Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range Signature
|
// Range Signature
|
||||||
|
@ -57,22 +44,30 @@ type RangeSig struct {
|
||||||
ci Key64
|
ci Key64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ring Confidential Signature
|
// Confidential Transaction Keys
|
||||||
|
type CtKey struct {
|
||||||
|
destination Key
|
||||||
|
mask Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring Confidential Signature parts that we have to keep
|
||||||
type RctSigBase struct {
|
type RctSigBase struct {
|
||||||
sigType uint8
|
sigType uint8
|
||||||
message Key
|
message Key
|
||||||
mixRing CtKeyM
|
mixRing [][]CtKey
|
||||||
pseudoOuts KeyV
|
pseudoOuts []Key
|
||||||
ecdhInfo []ecdhTuple
|
ecdhInfo []ecdhTuple
|
||||||
outPk CtKeyV
|
outPk []CtKey
|
||||||
txFee uint64
|
txFee uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ring Confidential Signature parts that we can just prune later
|
||||||
type RctSigPrunable struct {
|
type RctSigPrunable struct {
|
||||||
rangeSigs []RangeSig
|
rangeSigs []RangeSig
|
||||||
mgSigs []MgSig
|
mlsagSigs []MlsagSig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ring Confidential Signature struct that can verify everything
|
||||||
type RctSig struct {
|
type RctSig struct {
|
||||||
RctSigBase
|
RctSigBase
|
||||||
RctSigPrunable
|
RctSigPrunable
|
||||||
|
@ -133,7 +128,7 @@ func AddKeys2(result, a, b, B *Key) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// subtract two points together
|
// subtract two points A - B
|
||||||
func SubKeys(diff, k1, k2 *Key) {
|
func SubKeys(diff, k1, k2 *Key) {
|
||||||
a := k1.ToExtended()
|
a := k1.ToExtended()
|
||||||
b := new(CachedGroupElement)
|
b := new(CachedGroupElement)
|
||||||
|
@ -164,7 +159,7 @@ func (r *RangeSig) Serialize() (result []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MgSig) Serialize() (result []byte) {
|
func (m *MlsagSig) Serialize() (result []byte) {
|
||||||
for i := 0; i < len(m.ss); i++ {
|
for i := 0; i < len(m.ss); i++ {
|
||||||
for j := 0; j < len(m.ss[i]); j++ {
|
for j := 0; j < len(m.ss[i]); j++ {
|
||||||
result = append(result, m.ss[i][j][:]...)
|
result = append(result, m.ss[i][j][:]...)
|
||||||
|
@ -208,8 +203,8 @@ func (r *RctSig) SerializePrunable() (result []byte) {
|
||||||
for _, rangeSig := range r.rangeSigs {
|
for _, rangeSig := range r.rangeSigs {
|
||||||
result = append(result, rangeSig.Serialize()...)
|
result = append(result, rangeSig.Serialize()...)
|
||||||
}
|
}
|
||||||
for _, mgSig := range r.mgSigs {
|
for _, mlsagSig := range r.mlsagSigs {
|
||||||
result = append(result, mgSig.Serialize()...)
|
result = append(result, mlsagSig.Serialize()...)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -271,13 +266,13 @@ func (r *RctSig) VerifyRctSimple() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseKey(buf io.Reader) (result Key, err error) {
|
func (r *RctSig) VerifyRctFull() bool {
|
||||||
key := make([]byte, KeyLength)
|
for i, ctKey := range r.outPk {
|
||||||
if _, err = buf.Read(key); err != nil {
|
if !verRange(&ctKey.mask, r.rangeSigs[i]) {
|
||||||
return
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
copy(result[:], key)
|
return true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCtKey(buf io.Reader) (result CtKey, err error) {
|
func ParseCtKey(buf io.Reader) (result CtKey, err error) {
|
||||||
|
@ -373,18 +368,18 @@ func ParseRingCtSignature(buf io.Reader, nInputs, nOutputs, nMixin int) (result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.mgSigs = make([]MgSig, nMg)
|
r.mlsagSigs = make([]MlsagSig, nMg)
|
||||||
for i := 0; i < nMg; i++ {
|
for i := 0; i < nMg; i++ {
|
||||||
r.mgSigs[i].ss = make([]KeyV, nMixin+1)
|
r.mlsagSigs[i].ss = make([][]Key, nMixin+1)
|
||||||
for j := 0; j < nMixin+1; j++ {
|
for j := 0; j < nMixin+1; j++ {
|
||||||
r.mgSigs[i].ss[j] = make([]Key, nSS)
|
r.mlsagSigs[i].ss[j] = make([]Key, nSS)
|
||||||
for k := 0; k < nSS; k++ {
|
for k := 0; k < nSS; k++ {
|
||||||
if r.mgSigs[i].ss[j][k], err = ParseKey(buf); err != nil {
|
if r.mlsagSigs[i].ss[j][k], err = ParseKey(buf); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.mgSigs[i].cc, err = ParseKey(buf); err != nil {
|
if r.mlsagSigs[i].cc, err = ParseKey(buf); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
58
ringct_test.go
Normal file
58
ringct_test.go
Normal file
File diff suppressed because one or more lines are too long
|
@ -1819,8 +1819,7 @@ func TestCreateSignature(t *testing.T) {
|
||||||
privKey, _ := NewKeyPair()
|
privKey, _ := NewKeyPair()
|
||||||
mixins := make([]Key, numMixins)
|
mixins := make([]Key, numMixins)
|
||||||
for j := 0; j < numMixins; j++ {
|
for j := 0; j < numMixins; j++ {
|
||||||
_, pk := NewKeyPair()
|
mixins[j] = *RandomPubKey()
|
||||||
mixins[j] = *pk
|
|
||||||
}
|
}
|
||||||
keyImage, pubKeys, sig := CreateSignature(&hash, mixins, privKey)
|
keyImage, pubKeys, sig := CreateSignature(&hash, mixins, privKey)
|
||||||
if !VerifySignature(&hash, &keyImage, pubKeys, sig) {
|
if !VerifySignature(&hash, &keyImage, pubKeys, sig) {
|
||||||
|
|
|
@ -13,15 +13,6 @@ const (
|
||||||
|
|
||||||
var UnimplementedError = fmt.Errorf("Unimplemented")
|
var UnimplementedError = fmt.Errorf("Unimplemented")
|
||||||
|
|
||||||
type txOutToKey struct {
|
|
||||||
key Key
|
|
||||||
}
|
|
||||||
|
|
||||||
type TxOutTargetSerializer interface {
|
|
||||||
TargetSerialize() []byte
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type txInGen struct {
|
type txInGen struct {
|
||||||
height uint64
|
height uint64
|
||||||
}
|
}
|
||||||
|
@ -39,7 +30,7 @@ type TxInSerializer interface {
|
||||||
|
|
||||||
type TxOut struct {
|
type TxOut struct {
|
||||||
amount uint64
|
amount uint64
|
||||||
target TxOutTargetSerializer
|
key Key
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionPrefix struct {
|
type TransactionPrefix struct {
|
||||||
|
@ -54,6 +45,7 @@ type Transaction struct {
|
||||||
TransactionPrefix
|
TransactionPrefix
|
||||||
signatures []RingSignature
|
signatures []RingSignature
|
||||||
rctSignature *RctSig
|
rctSignature *RctSig
|
||||||
|
expanded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hash) Serialize() (result []byte) {
|
func (h *Hash) Serialize() (result []byte) {
|
||||||
|
@ -66,12 +58,13 @@ func (p *Key) Serialize() (result []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txOutToKey) TargetSerialize() (result []byte) {
|
func (t *TxOut) Serialize() (result []byte) {
|
||||||
result = append([]byte{txOutToKeyMarker}, t.key.Serialize()...)
|
result = append(Uint64ToBytes(t.amount), txOutToKeyMarker)
|
||||||
|
result = append(result, t.key[:]...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txOutToKey) String() (result string) {
|
func (t *TxOut) String() (result string) {
|
||||||
result = fmt.Sprintf("key: %x", t.key)
|
result = fmt.Sprintf("key: %x", t.key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -99,11 +92,6 @@ func (t *txInToKey) MixinLen() int {
|
||||||
return len(t.keyOffsets)
|
return len(t.keyOffsets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TxOut) Serialize() (result []byte) {
|
|
||||||
result = append(Uint64ToBytes(t.amount), t.target.TargetSerialize()...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TransactionPrefix) SerializePrefix() (result []byte) {
|
func (t *TransactionPrefix) SerializePrefix() (result []byte) {
|
||||||
result = append(Uint64ToBytes(uint64(t.version)), Uint64ToBytes(t.unlockTime)...)
|
result = append(Uint64ToBytes(uint64(t.version)), Uint64ToBytes(t.unlockTime)...)
|
||||||
result = append(result, Uint64ToBytes(uint64(len(t.vin)))...)
|
result = append(result, Uint64ToBytes(uint64(len(t.vin)))...)
|
||||||
|
@ -153,6 +141,49 @@ func (t *Transaction) SerializeBase() (result []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpandTransaction does nothing for version 1 transactions, but for version 2
|
||||||
|
// derives all the implied elements of the ring signature
|
||||||
|
func (t *Transaction) ExpandTransaction(outputKeys [][]CtKey) {
|
||||||
|
if t.version == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := t.rctSignature
|
||||||
|
if r.sigType == RCTTypeNull {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in the outPk property of the ring signature
|
||||||
|
for i, ctKey := range r.outPk {
|
||||||
|
ctKey.destination = t.vout[i].key
|
||||||
|
}
|
||||||
|
|
||||||
|
r.message = Key(t.PrefixHash())
|
||||||
|
if r.sigType == RCTTypeFull {
|
||||||
|
r.mixRing = make([][]CtKey, len(outputKeys[0]))
|
||||||
|
for i := 0; i < len(outputKeys); i++ {
|
||||||
|
r.mixRing[i] = make([]CtKey, len(outputKeys))
|
||||||
|
for j := 0; j < len(outputKeys[0]); j++ {
|
||||||
|
r.mixRing[j][i] = outputKeys[i][j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.mlsagSigs = make([]MlsagSig, 1)
|
||||||
|
r.mlsagSigs[0].ii = make([]Key, len(t.vin))
|
||||||
|
for i, txIn := range t.vin {
|
||||||
|
txInWithKey, _ := txIn.(*txInToKey)
|
||||||
|
r.mlsagSigs[0].ii[i] = txInWithKey.keyImage
|
||||||
|
}
|
||||||
|
} else if r.sigType == RCTTypeSimple {
|
||||||
|
r.mixRing = outputKeys
|
||||||
|
r.mlsagSigs = make([]MlsagSig, len(t.vin))
|
||||||
|
for i, txIn := range t.vin {
|
||||||
|
txInWithKey, _ := txIn.(*txInToKey)
|
||||||
|
r.mlsagSigs[i].ii = make([]Key, 1)
|
||||||
|
r.mlsagSigs[i].ii[0] = txInWithKey.keyImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.expanded = true
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Transaction) GetHash() (result Hash) {
|
func (t *Transaction) GetHash() (result Hash) {
|
||||||
if t.version == 1 {
|
if t.version == 1 {
|
||||||
result = Keccak256(t.Serialize())
|
result = Keccak256(t.Serialize())
|
||||||
|
@ -228,22 +259,6 @@ func ParseTxIn(buf io.Reader) (txIn TxInSerializer, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseTxOutToKey(buf io.Reader) (txOutTarget *txOutToKey, err error) {
|
|
||||||
t := new(txOutToKey)
|
|
||||||
pubKey := make([]byte, KeyLength)
|
|
||||||
n, err := buf.Read(pubKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n != KeyLength {
|
|
||||||
err = fmt.Errorf("Buffer not long enough for public key")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
copy(t.key[:], pubKey)
|
|
||||||
txOutTarget = t
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseTxOut(buf io.Reader) (txOut *TxOut, err error) {
|
func ParseTxOut(buf io.Reader) (txOut *TxOut, err error) {
|
||||||
t := new(TxOut)
|
t := new(TxOut)
|
||||||
t.amount, err = ReadVarInt(buf)
|
t.amount, err = ReadVarInt(buf)
|
||||||
|
@ -261,7 +276,7 @@ func ParseTxOut(buf io.Reader) (txOut *TxOut, err error) {
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case marker[0] == txOutToKeyMarker:
|
case marker[0] == txOutToKeyMarker:
|
||||||
t.target, err = ParseTxOutToKey(buf)
|
t.key, err = ParseKey(buf)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("Bad Marker")
|
err = fmt.Errorf("Bad Marker")
|
||||||
return
|
return
|
||||||
|
|
File diff suppressed because one or more lines are too long
Reference in a new issue