Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
G
Geth-Modification
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张蕾
Geth-Modification
Commits
7bbdf3e2
Unverified
Commit
7bbdf3e2
authored
Aug 10, 2017
by
Jeffrey Wilcke
Committed by
Péter Szilágyi
Aug 11, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
core: add Metropolis pre-compiles (EIP 197, 198 and 213)
parent
6ca59d98
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
242 additions
and
2 deletions
+242
-2
contracts.go
core/vm/contracts.go
+202
-0
contracts_test.go
core/vm/contracts_test.go
+34
-0
evm.go
core/vm/evm.go
+6
-2
No files found.
core/vm/contracts.go
View file @
7bbdf3e2
...
@@ -22,7 +22,9 @@ import (
...
@@ -22,7 +22,9 @@ import (
"math/big"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/bn256"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/params"
"golang.org/x/crypto/ripemd160"
"golang.org/x/crypto/ripemd160"
)
)
...
@@ -45,6 +47,19 @@ var PrecompiledContracts = map[common.Address]PrecompiledContract{
...
@@ -45,6 +47,19 @@ var PrecompiledContracts = map[common.Address]PrecompiledContract{
common
.
BytesToAddress
([]
byte
{
4
})
:
&
dataCopy
{},
common
.
BytesToAddress
([]
byte
{
4
})
:
&
dataCopy
{},
}
}
// PrecompiledContractsMetropolis contains the default set of ethereum contracts
// for metropolis hardfork
var
PrecompiledContractsMetropolis
=
map
[
common
.
Address
]
PrecompiledContract
{
common
.
BytesToAddress
([]
byte
{
1
})
:
&
ecrecover
{},
common
.
BytesToAddress
([]
byte
{
2
})
:
&
sha256hash
{},
common
.
BytesToAddress
([]
byte
{
3
})
:
&
ripemd160hash
{},
common
.
BytesToAddress
([]
byte
{
4
})
:
&
dataCopy
{},
common
.
BytesToAddress
([]
byte
{
5
})
:
&
bigModexp
{},
common
.
BytesToAddress
([]
byte
{
6
})
:
&
bn256Add
{},
common
.
BytesToAddress
([]
byte
{
7
})
:
&
bn256ScalarMul
{},
common
.
BytesToAddress
([]
byte
{
8
})
:
&
pairing
{},
}
// RunPrecompile runs and evaluate the output of a precompiled contract defined in contracts.go
// RunPrecompile runs and evaluate the output of a precompiled contract defined in contracts.go
func
RunPrecompiledContract
(
p
PrecompiledContract
,
input
[]
byte
,
contract
*
Contract
)
(
ret
[]
byte
,
err
error
)
{
func
RunPrecompiledContract
(
p
PrecompiledContract
,
input
[]
byte
,
contract
*
Contract
)
(
ret
[]
byte
,
err
error
)
{
gas
:=
p
.
RequiredGas
(
input
)
gas
:=
p
.
RequiredGas
(
input
)
...
@@ -132,3 +147,190 @@ func (c *dataCopy) RequiredGas(input []byte) uint64 {
...
@@ -132,3 +147,190 @@ func (c *dataCopy) RequiredGas(input []byte) uint64 {
func
(
c
*
dataCopy
)
Run
(
in
[]
byte
)
([]
byte
,
error
)
{
func
(
c
*
dataCopy
)
Run
(
in
[]
byte
)
([]
byte
,
error
)
{
return
in
,
nil
return
in
,
nil
}
}
// bigModexp implements a native big integer exponential modular operation.
type
bigModexp
struct
{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func
(
c
*
bigModexp
)
RequiredGas
(
input
[]
byte
)
uint64
{
// TODO reword required gas to have error reporting and convert arithmetic
// to uint64.
if
len
(
input
)
<
3
*
32
{
input
=
append
(
input
,
make
([]
byte
,
3
*
32
-
len
(
input
))
...
)
}
var
(
baseLen
=
new
(
big
.
Int
)
.
SetBytes
(
input
[
:
31
])
expLen
=
math
.
BigMax
(
new
(
big
.
Int
)
.
SetBytes
(
input
[
32
:
64
]),
big
.
NewInt
(
1
))
modLen
=
new
(
big
.
Int
)
.
SetBytes
(
input
[
65
:
97
])
)
x
:=
new
(
big
.
Int
)
.
Set
(
math
.
BigMax
(
baseLen
,
modLen
))
x
.
Mul
(
x
,
x
)
x
.
Mul
(
x
,
expLen
)
x
.
Div
(
x
,
new
(
big
.
Int
)
.
SetUint64
(
params
.
QuadCoeffDiv
))
return
x
.
Uint64
()
}
func
(
c
*
bigModexp
)
Run
(
input
[]
byte
)
([]
byte
,
error
)
{
if
len
(
input
)
<
3
*
32
{
input
=
append
(
input
,
make
([]
byte
,
3
*
32
-
len
(
input
))
...
)
}
// why 32-byte? These values won't fit anyway
var
(
baseLen
=
new
(
big
.
Int
)
.
SetBytes
(
input
[
:
32
])
.
Uint64
()
expLen
=
new
(
big
.
Int
)
.
SetBytes
(
input
[
32
:
64
])
.
Uint64
()
modLen
=
new
(
big
.
Int
)
.
SetBytes
(
input
[
64
:
96
])
.
Uint64
()
)
input
=
input
[
96
:
]
if
uint64
(
len
(
input
))
<
baseLen
{
input
=
append
(
input
,
make
([]
byte
,
baseLen
-
uint64
(
len
(
input
)))
...
)
}
base
:=
new
(
big
.
Int
)
.
SetBytes
(
input
[
:
baseLen
])
input
=
input
[
baseLen
:
]
if
uint64
(
len
(
input
))
<
expLen
{
input
=
append
(
input
,
make
([]
byte
,
expLen
-
uint64
(
len
(
input
)))
...
)
}
exp
:=
new
(
big
.
Int
)
.
SetBytes
(
input
[
:
expLen
])
input
=
input
[
expLen
:
]
if
uint64
(
len
(
input
))
<
modLen
{
input
=
append
(
input
,
make
([]
byte
,
modLen
-
uint64
(
len
(
input
)))
...
)
}
mod
:=
new
(
big
.
Int
)
.
SetBytes
(
input
[
:
modLen
])
return
common
.
LeftPadBytes
(
base
.
Exp
(
base
,
exp
,
mod
)
.
Bytes
(),
len
(
input
[
:
modLen
])),
nil
}
type
bn256Add
struct
{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func
(
c
*
bn256Add
)
RequiredGas
(
input
[]
byte
)
uint64
{
return
0
// TODO
}
func
(
c
*
bn256Add
)
Run
(
in
[]
byte
)
([]
byte
,
error
)
{
in
=
common
.
RightPadBytes
(
in
,
128
)
x
,
onCurve
:=
new
(
bn256
.
G1
)
.
Unmarshal
(
in
[
:
64
])
if
!
onCurve
{
return
nil
,
errNotOnCurve
}
gx
,
gy
,
_
,
_
:=
x
.
CurvePoints
()
if
gx
.
Cmp
(
bn256
.
P
)
>=
0
||
gy
.
Cmp
(
bn256
.
P
)
>=
0
{
return
nil
,
errInvalidCurvePoint
}
y
,
onCurve
:=
new
(
bn256
.
G1
)
.
Unmarshal
(
in
[
64
:
128
])
if
!
onCurve
{
return
nil
,
errNotOnCurve
}
gx
,
gy
,
_
,
_
=
y
.
CurvePoints
()
if
gx
.
Cmp
(
bn256
.
P
)
>=
0
||
gy
.
Cmp
(
bn256
.
P
)
>=
0
{
return
nil
,
errInvalidCurvePoint
}
x
.
Add
(
x
,
y
)
return
x
.
Marshal
(),
nil
}
type
bn256ScalarMul
struct
{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func
(
c
*
bn256ScalarMul
)
RequiredGas
(
input
[]
byte
)
uint64
{
return
0
// TODO
}
func
(
c
*
bn256ScalarMul
)
Run
(
in
[]
byte
)
([]
byte
,
error
)
{
in
=
common
.
RightPadBytes
(
in
,
96
)
g1
,
onCurve
:=
new
(
bn256
.
G1
)
.
Unmarshal
(
in
[
:
64
])
if
!
onCurve
{
return
nil
,
errNotOnCurve
}
x
,
y
,
_
,
_
:=
g1
.
CurvePoints
()
if
x
.
Cmp
(
bn256
.
P
)
>=
0
||
y
.
Cmp
(
bn256
.
P
)
>=
0
{
return
nil
,
errInvalidCurvePoint
}
g1
.
ScalarMult
(
g1
,
new
(
big
.
Int
)
.
SetBytes
(
in
[
64
:
96
]))
return
g1
.
Marshal
(),
nil
}
// pairing implements a pairing pre-compile for the bn256 curve
type
pairing
struct
{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func
(
c
*
pairing
)
RequiredGas
(
input
[]
byte
)
uint64
{
//return 0 // TODO
k
:=
(
len
(
input
)
+
191
)
/
pairSize
return
uint64
(
60000
*
k
+
40000
)
}
const
pairSize
=
192
var
(
true32Byte
=
[]
byte
{
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
1
}
fals32Byte
=
make
([]
byte
,
32
)
errNotOnCurve
=
errors
.
New
(
"point not on elliptic curve"
)
errInvalidCurvePoint
=
errors
.
New
(
"invalid elliptic curve point"
)
)
func
(
c
*
pairing
)
Run
(
in
[]
byte
)
([]
byte
,
error
)
{
if
len
(
in
)
==
0
{
return
true32Byte
,
nil
}
if
len
(
in
)
%
pairSize
>
0
{
return
nil
,
errBadPrecompileInput
}
var
(
g1s
[]
*
bn256
.
G1
g2s
[]
*
bn256
.
G2
)
for
i
:=
0
;
i
<
len
(
in
);
i
+=
pairSize
{
g1
,
onCurve
:=
new
(
bn256
.
G1
)
.
Unmarshal
(
in
[
i
:
i
+
64
])
if
!
onCurve
{
return
nil
,
errNotOnCurve
}
x
,
y
,
_
,
_
:=
g1
.
CurvePoints
()
if
x
.
Cmp
(
bn256
.
P
)
>=
0
||
y
.
Cmp
(
bn256
.
P
)
>=
0
{
return
nil
,
errInvalidCurvePoint
}
g2
,
onCurve
:=
new
(
bn256
.
G2
)
.
Unmarshal
(
in
[
i
+
64
:
i
+
192
])
if
!
onCurve
{
return
nil
,
errNotOnCurve
}
x2
,
y2
,
_
,
_
:=
g2
.
CurvePoints
()
if
x2
.
Real
()
.
Cmp
(
bn256
.
P
)
>=
0
||
x2
.
Imag
()
.
Cmp
(
bn256
.
P
)
>=
0
||
y2
.
Real
()
.
Cmp
(
bn256
.
P
)
>=
0
||
y2
.
Imag
()
.
Cmp
(
bn256
.
P
)
>=
0
{
return
nil
,
errInvalidCurvePoint
}
g1s
=
append
(
g1s
,
g1
)
g2s
=
append
(
g2s
,
g2
)
}
isOne
:=
bn256
.
PairingCheck
(
g1s
,
g2s
)
if
isOne
{
return
true32Byte
,
nil
}
return
fals32Byte
,
nil
}
core/vm/contracts_test.go
View file @
7bbdf3e2
package
vm
package
vm
import
(
"testing"
"github.com/ethereum/go-ethereum/common"
)
const
input
=
""
func
TestPairing
(
t
*
testing
.
T
)
{
pairing
:=
&
pairing
{}
for
i
,
test
:=
range
[]
struct
{
input
string
valid
int
}{
{
"1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
,
1
},
{
"2eca0c7238bf16e83e7a1e6c5d49540685ff51380f309842a98561558019fc0203d3260361bb8451de5ff5ecd17f010ff22f5c31cdf184e9020b06fa5997db841213d2149b006137fcfb23036606f848d638d576a120ca981b5b1a5f9300b3ee2276cf730cf493cd95d64677bbb75fc42db72513a4c1e387b476d056f80aa75f21ee6226d31426322afcda621464d0611d226783262e21bb3bc86b537e986237096df1f82dff337dd5972e32a8ad43e28a78a96a823ef1cd4debe12b6552ea5f06967a1237ebfeca9aaae0d6d0bab8e28c198c5a339ef8a2407e31cdac516db922160fa257a5fd5b280642ff47b65eca77e626cb685c84fa6d3b6882a283ddd1198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
,
1
},
{
"0f25929bcb43d5a57391564615c9e70a992b10eafa4db109709649cf48c50dd216da2f5cb6be7a0aa72c440c53c9bbdfec6c36c7d515536431b3a865468acbba2e89718ad33c8bed92e210e81d1853435399a271913a6520736a4729cf0d51eb01a9e2ffa2e92599b68e44de5bcf354fa2642bd4f26b259daa6f7ce3ed57aeb314a9a87b789a58af499b314e13c3d65bede56c07ea2d418d6874857b70763713178fb49a2d6cd347dc58973ff49613a20757d0fcc22079f9abd10c3baee245901b9e027bd5cfc2cb5db82d4dc9677ac795ec500ecd47deee3b5da006d6d049b811d7511c78158de484232fc68daf8a45cf217d1c2fae693ff5871e8752d73b21198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
,
1
},
{
"2f2ea0b3da1e8ef11914acf8b2e1b32d99df51f5f4f206fc6b947eae860eddb6068134ddb33dc888ef446b648d72338684d678d2eb2371c61a50734d78da4b7225f83c8b6ab9de74e7da488ef02645c5a16a6652c3c71a15dc37fe3a5dcb7cb122acdedd6308e3bb230d226d16a105295f523a8a02bfc5e8bd2da135ac4c245d065bbad92e7c4e31bf3757f1fe7362a63fbfee50e7dc68da116e67d600d9bf6806d302580dc0661002994e7cd3a7f224e7ddc27802777486bf80f40e4ca3cfdb186bac5188a98c45e6016873d107f5cd131f3a3e339d0375e58bd6219347b008122ae2b09e539e152ec5364e7e2204b03d11d3caa038bfc7cd499f8176aacbee1f39e4e4afc4bc74790a4a028aff2c3d2538731fb755edefd8cb48d6ea589b5e283f150794b6736f670d6a1033f9b46c6f5204f50813eb85c8dc4b59db1c5d39140d97ee4d2b36d99bc49974d18ecca3e7ad51011956051b464d9e27d46cc25e0764bb98575bd466d32db7b15f582b2d5c452b36aa394b789366e5e3ca5aabd415794ab061441e51d01e94640b7e3084a07e02c78cf3103c542bc5b298669f211b88da1679b0b64a63b7e0e7bfe52aae524f73a55be7fe70c7e9bfc94b4cf0da1213d2149b006137fcfb23036606f848d638d576a120ca981b5b1a5f9300b3ee2276cf730cf493cd95d64677bbb75fc42db72513a4c1e387b476d056f80aa75f21ee6226d31426322afcda621464d0611d226783262e21bb3bc86b537e986237096df1f82dff337dd5972e32a8ad43e28a78a96a823ef1cd4debe12b6552ea5f"
,
1
},
{
"20a754d2071d4d53903e3b31a7e98ad6882d58aec240ef981fdf0a9d22c5926a29c853fcea789887315916bbeb89ca37edb355b4f980c9a12a94f30deeed30211213d2149b006137fcfb23036606f848d638d576a120ca981b5b1a5f9300b3ee2276cf730cf493cd95d64677bbb75fc42db72513a4c1e387b476d056f80aa75f21ee6226d31426322afcda621464d0611d226783262e21bb3bc86b537e986237096df1f82dff337dd5972e32a8ad43e28a78a96a823ef1cd4debe12b6552ea5f1abb4a25eb9379ae96c84fff9f0540abcfc0a0d11aeda02d4f37e4baf74cb0c11073b3ff2cdbb38755f8691ea59e9606696b3ff278acfc098fa8226470d03869217cee0a9ad79a4493b5253e2e4e3a39fc2df38419f230d341f60cb064a0ac290a3d76f140db8418ba512272381446eb73958670f00cf46f1d9e64cba057b53c26f64a8ec70387a13e41430ed3ee4a7db2059cc5fc13c067194bcc0cb49a98552fd72bd9edb657346127da132e5b82ab908f5816c826acb499e22f2412d1a2d70f25929bcb43d5a57391564615c9e70a992b10eafa4db109709649cf48c50dd2198a1f162a73261f112401aa2db79c7dab1533c9935c77290a6ce3b191f2318d198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
,
1
},
{
"1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c103188585e2364128fe25c70558f1560f4f9350baf3959e603cc91486e110936198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
,
0
},
}
{
r
,
err
:=
pairing
.
Run
(
common
.
FromHex
(
test
.
input
))
if
err
!=
nil
{
t
.
Error
(
i
,
":"
,
err
)
}
if
int
(
r
[
31
])
!=
test
.
valid
{
t
.
Error
(
i
,
"expected"
,
test
.
valid
,
"but was"
,
r
[
31
])
}
}
}
core/vm/evm.go
View file @
7bbdf3e2
...
@@ -37,6 +37,10 @@ type (
...
@@ -37,6 +37,10 @@ type (
func
run
(
evm
*
EVM
,
snapshot
int
,
contract
*
Contract
,
input
[]
byte
)
([]
byte
,
error
)
{
func
run
(
evm
*
EVM
,
snapshot
int
,
contract
*
Contract
,
input
[]
byte
)
([]
byte
,
error
)
{
if
contract
.
CodeAddr
!=
nil
{
if
contract
.
CodeAddr
!=
nil
{
precompiledContracts
:=
PrecompiledContracts
precompiledContracts
:=
PrecompiledContracts
if
evm
.
ChainConfig
()
.
IsMetropolis
(
evm
.
BlockNumber
)
{
precompiledContracts
=
PrecompiledContractsMetropolis
}
if
p
:=
precompiledContracts
[
*
contract
.
CodeAddr
];
p
!=
nil
{
if
p
:=
precompiledContracts
[
*
contract
.
CodeAddr
];
p
!=
nil
{
return
RunPrecompiledContract
(
p
,
input
,
contract
)
return
RunPrecompiledContract
(
p
,
input
,
contract
)
}
}
...
@@ -100,8 +104,8 @@ type EVM struct {
...
@@ -100,8 +104,8 @@ type EVM struct {
abort
int32
abort
int32
}
}
// NewEVM retutrns a new EVM
evmironment. The returned EVM is not thread safe
// NewEVM retutrns a new EVM
. The returned EVM is not thread safe and should
//
and should
only ever be used *once*.
// only ever be used *once*.
func
NewEVM
(
ctx
Context
,
statedb
StateDB
,
chainConfig
*
params
.
ChainConfig
,
vmConfig
Config
)
*
EVM
{
func
NewEVM
(
ctx
Context
,
statedb
StateDB
,
chainConfig
*
params
.
ChainConfig
,
vmConfig
Config
)
*
EVM
{
evm
:=
&
EVM
{
evm
:=
&
EVM
{
Context
:
ctx
,
Context
:
ctx
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment