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
9f6af6f8
Commit
9f6af6f8
authored
7 years ago
by
Eli
Committed by
Guillaume Ballet
7 years ago
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
whisper: Golint fixes in whisper packages (#16637)
parent
1da33028
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
74 additions
and
78 deletions
+74
-78
client.go
whisper/shhclient/client.go
+1
-2
api.go
whisper/whisperv5/api.go
+2
-2
doc.go
whisper/whisperv5/doc.go
+1
-1
message.go
whisper/whisperv5/message.go
+3
-3
peer.go
whisper/whisperv5/peer.go
+26
-26
peer_test.go
whisper/whisperv5/peer_test.go
+3
-3
topic.go
whisper/whisperv5/topic.go
+1
-1
whisper.go
whisper/whisperv5/whisper.go
+37
-40
No files found.
whisper/shhclient/client.go
View file @
9f6af6f8
...
...
@@ -67,7 +67,6 @@ func (sc *Client) SetMaxMessageSize(ctx context.Context, size uint32) error {
}
// SetMinimumPoW (experimental) sets the minimal PoW required by this node.
// This experimental function was introduced for the future dynamic adjustment of
// PoW requirement. If the node is overwhelmed with messages, it should raise the
// PoW requirement and notify the peers. The new value should be set relative to
...
...
@@ -77,7 +76,7 @@ func (sc *Client) SetMinimumPoW(ctx context.Context, pow float64) error {
return
sc
.
c
.
CallContext
(
ctx
,
&
ignored
,
"shh_setMinPoW"
,
pow
)
}
// Marks specific peer trusted, which will allow it to send historic (expired) messages.
// Mark
TrustedPeer mark
s specific peer trusted, which will allow it to send historic (expired) messages.
// Note This function is not adding new nodes, the node needs to exists as a peer.
func
(
sc
*
Client
)
MarkTrustedPeer
(
ctx
context
.
Context
,
enode
string
)
error
{
var
ignored
bool
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/api.go
View file @
9f6af6f8
...
...
@@ -89,7 +89,7 @@ func (api *PublicWhisperAPI) SetMaxMessageSize(ctx context.Context, size uint32)
return
true
,
api
.
w
.
SetMaxMessageSize
(
size
)
}
// SetMinPo
w
sets the minimum PoW for a message before it is accepted.
// SetMinPo
W
sets the minimum PoW for a message before it is accepted.
func
(
api
*
PublicWhisperAPI
)
SetMinPoW
(
ctx
context
.
Context
,
pow
float64
)
(
bool
,
error
)
{
return
true
,
api
.
w
.
SetMinimumPoW
(
pow
)
}
...
...
@@ -142,7 +142,7 @@ func (api *PublicWhisperAPI) GetPublicKey(ctx context.Context, id string) (hexut
return
crypto
.
FromECDSAPub
(
&
key
.
PublicKey
),
nil
}
// GetP
ublic
Key returns the private key associated with the given key. The key is the hex
// GetP
rivate
Key returns the private key associated with the given key. The key is the hex
// encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62.
func
(
api
*
PublicWhisperAPI
)
GetPrivateKey
(
ctx
context
.
Context
,
id
string
)
(
hexutil
.
Bytes
,
error
)
{
key
,
err
:=
api
.
w
.
GetPrivateKey
(
id
)
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/doc.go
View file @
9f6af6f8
...
...
@@ -15,7 +15,7 @@
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
Package whisper implements the Whisper protocol (version 5).
Package whisper
v5
implements the Whisper protocol (version 5).
Whisper combines aspects of both DHTs and datagram messaging systems (e.g. UDP).
As such it may be likened and compared to both, not dissimilar to the
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/message.go
View file @
9f6af6f8
...
...
@@ -33,7 +33,7 @@ import (
"github.com/ethereum/go-ethereum/log"
)
//
Option
s specifies the exact way a message should be wrapped into an Envelope.
//
MessageParam
s specifies the exact way a message should be wrapped into an Envelope.
type
MessageParams
struct
{
TTL
uint32
Src
*
ecdsa
.
PrivateKey
...
...
@@ -86,7 +86,7 @@ func (msg *ReceivedMessage) isAsymmetricEncryption() bool {
return
msg
.
Dst
!=
nil
}
// NewMessage creates and initializes a non-signed, non-encrypted Whisper message.
// New
Sent
Message creates and initializes a non-signed, non-encrypted Whisper message.
func
NewSentMessage
(
params
*
MessageParams
)
(
*
sentMessage
,
error
)
{
msg
:=
sentMessage
{}
msg
.
Raw
=
make
([]
byte
,
1
,
len
(
params
.
Payload
)
+
len
(
params
.
Padding
)
+
signatureLength
+
padSizeLimit
)
...
...
@@ -330,7 +330,7 @@ func (msg *ReceivedMessage) extractPadding(end int) (int, bool) {
return
paddingSize
,
true
}
//
Recover
retrieves the public key of the message signer.
//
SigToPubKey
retrieves the public key of the message signer.
func
(
msg
*
ReceivedMessage
)
SigToPubKey
()
*
ecdsa
.
PublicKey
{
defer
func
()
{
recover
()
}()
// in case of invalid signature
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/peer.go
View file @
9f6af6f8
...
...
@@ -27,7 +27,7 @@ import (
set
"gopkg.in/fatih/set.v0"
)
//
p
eer represents a whisper protocol peer connection.
//
P
eer represents a whisper protocol peer connection.
type
Peer
struct
{
host
*
Whisper
peer
*
p2p
.
Peer
...
...
@@ -53,51 +53,51 @@ func newPeer(host *Whisper, remote *p2p.Peer, rw p2p.MsgReadWriter) *Peer {
// start initiates the peer updater, periodically broadcasting the whisper packets
// into the network.
func
(
p
*
Peer
)
start
()
{
go
p
.
update
()
log
.
Trace
(
"start"
,
"peer"
,
p
.
ID
())
func
(
p
eer
*
Peer
)
start
()
{
go
p
eer
.
update
()
log
.
Trace
(
"start"
,
"peer"
,
p
eer
.
ID
())
}
// stop terminates the peer updater, stopping message forwarding to it.
func
(
p
*
Peer
)
stop
()
{
close
(
p
.
quit
)
log
.
Trace
(
"stop"
,
"peer"
,
p
.
ID
())
func
(
p
eer
*
Peer
)
stop
()
{
close
(
p
eer
.
quit
)
log
.
Trace
(
"stop"
,
"peer"
,
p
eer
.
ID
())
}
// handshake sends the protocol initiation status message to the remote peer and
// verifies the remote status too.
func
(
p
*
Peer
)
handshake
()
error
{
func
(
p
eer
*
Peer
)
handshake
()
error
{
// Send the handshake status message asynchronously
errc
:=
make
(
chan
error
,
1
)
go
func
()
{
errc
<-
p2p
.
Send
(
p
.
ws
,
statusCode
,
ProtocolVersion
)
errc
<-
p2p
.
Send
(
p
eer
.
ws
,
statusCode
,
ProtocolVersion
)
}()
// Fetch the remote status packet and verify protocol match
packet
,
err
:=
p
.
ws
.
ReadMsg
()
packet
,
err
:=
p
eer
.
ws
.
ReadMsg
()
if
err
!=
nil
{
return
err
}
if
packet
.
Code
!=
statusCode
{
return
fmt
.
Errorf
(
"peer [%x] sent packet %x before status packet"
,
p
.
ID
(),
packet
.
Code
)
return
fmt
.
Errorf
(
"peer [%x] sent packet %x before status packet"
,
p
eer
.
ID
(),
packet
.
Code
)
}
s
:=
rlp
.
NewStream
(
packet
.
Payload
,
uint64
(
packet
.
Size
))
peerVersion
,
err
:=
s
.
Uint
()
if
err
!=
nil
{
return
fmt
.
Errorf
(
"peer [%x] sent bad status message: %v"
,
p
.
ID
(),
err
)
return
fmt
.
Errorf
(
"peer [%x] sent bad status message: %v"
,
p
eer
.
ID
(),
err
)
}
if
peerVersion
!=
ProtocolVersion
{
return
fmt
.
Errorf
(
"peer [%x]: protocol version mismatch %d != %d"
,
p
.
ID
(),
peerVersion
,
ProtocolVersion
)
return
fmt
.
Errorf
(
"peer [%x]: protocol version mismatch %d != %d"
,
p
eer
.
ID
(),
peerVersion
,
ProtocolVersion
)
}
// Wait until out own status is consumed too
if
err
:=
<-
errc
;
err
!=
nil
{
return
fmt
.
Errorf
(
"peer [%x] failed to send status packet: %v"
,
p
.
ID
(),
err
)
return
fmt
.
Errorf
(
"peer [%x] failed to send status packet: %v"
,
p
eer
.
ID
(),
err
)
}
return
nil
}
// update executes periodic operations on the peer, including message transmission
// and expiration.
func
(
p
*
Peer
)
update
()
{
func
(
p
eer
*
Peer
)
update
()
{
// Start the tickers for the updates
expire
:=
time
.
NewTicker
(
expirationCycle
)
transmit
:=
time
.
NewTicker
(
transmissionCycle
)
...
...
@@ -106,15 +106,15 @@ func (p *Peer) update() {
for
{
select
{
case
<-
expire
.
C
:
p
.
expire
()
p
eer
.
expire
()
case
<-
transmit
.
C
:
if
err
:=
p
.
broadcast
();
err
!=
nil
{
log
.
Trace
(
"broadcast failed"
,
"reason"
,
err
,
"peer"
,
p
.
ID
())
if
err
:=
p
eer
.
broadcast
();
err
!=
nil
{
log
.
Trace
(
"broadcast failed"
,
"reason"
,
err
,
"peer"
,
p
eer
.
ID
())
return
}
case
<-
p
.
quit
:
case
<-
p
eer
.
quit
:
return
}
}
...
...
@@ -148,16 +148,16 @@ func (peer *Peer) expire() {
// broadcast iterates over the collection of envelopes and transmits yet unknown
// ones over the network.
func
(
p
*
Peer
)
broadcast
()
error
{
func
(
p
eer
*
Peer
)
broadcast
()
error
{
var
cnt
int
envelopes
:=
p
.
host
.
Envelopes
()
envelopes
:=
p
eer
.
host
.
Envelopes
()
for
_
,
envelope
:=
range
envelopes
{
if
!
p
.
marked
(
envelope
)
{
err
:=
p2p
.
Send
(
p
.
ws
,
messagesCode
,
envelope
)
if
!
p
eer
.
marked
(
envelope
)
{
err
:=
p2p
.
Send
(
p
eer
.
ws
,
messagesCode
,
envelope
)
if
err
!=
nil
{
return
err
}
else
{
p
.
mark
(
envelope
)
p
eer
.
mark
(
envelope
)
cnt
++
}
}
...
...
@@ -168,7 +168,7 @@ func (p *Peer) broadcast() error {
return
nil
}
func
(
p
*
Peer
)
ID
()
[]
byte
{
id
:=
p
.
peer
.
ID
()
func
(
p
eer
*
Peer
)
ID
()
[]
byte
{
id
:=
p
eer
.
peer
.
ID
()
return
id
[
:
]
}
This diff is collapsed.
Click to expand it.
whisper/whisperv5/peer_test.go
View file @
9f6af6f8
...
...
@@ -32,7 +32,7 @@ import (
"github.com/ethereum/go-ethereum/p2p/nat"
)
var
keys
[]
string
=
[]
string
{
var
keys
=
[]
string
{
"d49dcf37238dc8a7aac57dc61b9fee68f0a97f062968978b9fafa7d1033d03a9"
,
"73fd6143c48e80ed3c56ea159fe7494a0b6b393a392227b422f4c3e8f1b54f98"
,
"119dd32adb1daa7a4c7bf77f847fb28730785aa92947edf42fdd997b54de40dc"
,
...
...
@@ -84,9 +84,9 @@ type TestNode struct {
var
result
TestData
var
nodes
[
NumNodes
]
*
TestNode
var
sharedKey
[]
byte
=
[]
byte
(
"some arbitrary data here"
)
var
sharedKey
=
[]
byte
(
"some arbitrary data here"
)
var
sharedTopic
TopicType
=
TopicType
{
0xF
,
0x1
,
0x2
,
0
}
var
expectedMessage
[]
byte
=
[]
byte
(
"per rectum ad astra"
)
var
expectedMessage
=
[]
byte
(
"per rectum ad astra"
)
// This test does the following:
// 1. creates a chain of whisper nodes,
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/topic.go
View file @
9f6af6f8
...
...
@@ -23,7 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
)
// Topic represents a cryptographically secure, probabilistic partial
// Topic
Type
represents a cryptographically secure, probabilistic partial
// classifications of a message, determined as the first (left) 4 bytes of the
// SHA3 hash of some arbitrary data given by the original author of the message.
type
TopicType
[
TopicLength
]
byte
...
...
This diff is collapsed.
Click to expand it.
whisper/whisperv5/whisper.go
View file @
9f6af6f8
...
...
@@ -469,18 +469,18 @@ func (w *Whisper) Stop() error {
// HandlePeer is called by the underlying P2P layer when the whisper sub-protocol
// connection is negotiated.
func
(
w
h
*
Whisper
)
HandlePeer
(
peer
*
p2p
.
Peer
,
rw
p2p
.
MsgReadWriter
)
error
{
func
(
w
*
Whisper
)
HandlePeer
(
peer
*
p2p
.
Peer
,
rw
p2p
.
MsgReadWriter
)
error
{
// Create the new peer and start tracking it
whisperPeer
:=
newPeer
(
w
h
,
peer
,
rw
)
whisperPeer
:=
newPeer
(
w
,
peer
,
rw
)
w
h
.
peerMu
.
Lock
()
w
h
.
peers
[
whisperPeer
]
=
struct
{}{}
w
h
.
peerMu
.
Unlock
()
w
.
peerMu
.
Lock
()
w
.
peers
[
whisperPeer
]
=
struct
{}{}
w
.
peerMu
.
Unlock
()
defer
func
()
{
w
h
.
peerMu
.
Lock
()
delete
(
w
h
.
peers
,
whisperPeer
)
w
h
.
peerMu
.
Unlock
()
w
.
peerMu
.
Lock
()
delete
(
w
.
peers
,
whisperPeer
)
w
.
peerMu
.
Unlock
()
}()
// Run the peer handshake and state updates
...
...
@@ -490,11 +490,11 @@ func (wh *Whisper) HandlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error {
whisperPeer
.
start
()
defer
whisperPeer
.
stop
()
return
w
h
.
runMessageLoop
(
whisperPeer
,
rw
)
return
w
.
runMessageLoop
(
whisperPeer
,
rw
)
}
// runMessageLoop reads and processes inbound messages directly to merge into client-global state.
func
(
w
h
*
Whisper
)
runMessageLoop
(
p
*
Peer
,
rw
p2p
.
MsgReadWriter
)
error
{
func
(
w
*
Whisper
)
runMessageLoop
(
p
*
Peer
,
rw
p2p
.
MsgReadWriter
)
error
{
for
{
// fetch the next packet
packet
,
err
:=
rw
.
ReadMsg
()
...
...
@@ -502,7 +502,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
log
.
Warn
(
"message loop"
,
"peer"
,
p
.
peer
.
ID
(),
"err"
,
err
)
return
err
}
if
packet
.
Size
>
w
h
.
MaxMessageSize
()
{
if
packet
.
Size
>
w
.
MaxMessageSize
()
{
log
.
Warn
(
"oversized message received"
,
"peer"
,
p
.
peer
.
ID
())
return
errors
.
New
(
"oversized message received"
)
}
...
...
@@ -518,7 +518,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
log
.
Warn
(
"failed to decode envelope, peer will be disconnected"
,
"peer"
,
p
.
peer
.
ID
(),
"err"
,
err
)
return
errors
.
New
(
"invalid envelope"
)
}
cached
,
err
:=
w
h
.
add
(
&
envelope
)
cached
,
err
:=
w
.
add
(
&
envelope
)
if
err
!=
nil
{
log
.
Warn
(
"bad envelope received, peer will be disconnected"
,
"peer"
,
p
.
peer
.
ID
(),
"err"
,
err
)
return
errors
.
New
(
"invalid envelope"
)
...
...
@@ -537,17 +537,17 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
log
.
Warn
(
"failed to decode direct message, peer will be disconnected"
,
"peer"
,
p
.
peer
.
ID
(),
"err"
,
err
)
return
errors
.
New
(
"invalid direct message"
)
}
w
h
.
postEvent
(
&
envelope
,
true
)
w
.
postEvent
(
&
envelope
,
true
)
}
case
p2pRequestCode
:
// Must be processed if mail server is implemented. Otherwise ignore.
if
w
h
.
mailServer
!=
nil
{
if
w
.
mailServer
!=
nil
{
var
request
Envelope
if
err
:=
packet
.
Decode
(
&
request
);
err
!=
nil
{
log
.
Warn
(
"failed to decode p2p request message, peer will be disconnected"
,
"peer"
,
p
.
peer
.
ID
(),
"err"
,
err
)
return
errors
.
New
(
"invalid p2p request"
)
}
w
h
.
mailServer
.
DeliverMail
(
p
,
&
request
)
w
.
mailServer
.
DeliverMail
(
p
,
&
request
)
}
default
:
// New message types might be implemented in the future versions of Whisper.
...
...
@@ -561,29 +561,27 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
// add inserts a new envelope into the message pool to be distributed within the
// whisper network. It also inserts the envelope into the expiration pool at the
// appropriate time-stamp. In case of error, connection should be dropped.
func
(
w
h
*
Whisper
)
add
(
envelope
*
Envelope
)
(
bool
,
error
)
{
func
(
w
*
Whisper
)
add
(
envelope
*
Envelope
)
(
bool
,
error
)
{
now
:=
uint32
(
time
.
Now
()
.
Unix
())
sent
:=
envelope
.
Expiry
-
envelope
.
TTL
if
sent
>
now
{
if
sent
-
SynchAllowance
>
now
{
return
false
,
fmt
.
Errorf
(
"envelope created in the future [%x]"
,
envelope
.
Hash
())
}
else
{
// recalculate PoW, adjusted for the time difference, plus one second for latency
envelope
.
calculatePoW
(
sent
-
now
+
1
)
}
// recalculate PoW, adjusted for the time difference, plus one second for latency
envelope
.
calculatePoW
(
sent
-
now
+
1
)
}
if
envelope
.
Expiry
<
now
{
if
envelope
.
Expiry
+
SynchAllowance
*
2
<
now
{
return
false
,
fmt
.
Errorf
(
"very old message"
)
}
else
{
log
.
Debug
(
"expired envelope dropped"
,
"hash"
,
envelope
.
Hash
()
.
Hex
())
return
false
,
nil
// drop envelope without error
}
log
.
Debug
(
"expired envelope dropped"
,
"hash"
,
envelope
.
Hash
()
.
Hex
())
return
false
,
nil
// drop envelope without error
}
if
uint32
(
envelope
.
size
())
>
w
h
.
MaxMessageSize
()
{
if
uint32
(
envelope
.
size
())
>
w
.
MaxMessageSize
()
{
return
false
,
fmt
.
Errorf
(
"huge messages are not allowed [%x]"
,
envelope
.
Hash
())
}
...
...
@@ -598,36 +596,36 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) {
return
false
,
fmt
.
Errorf
(
"wrong size of AESNonce: %d bytes [env: %x]"
,
aesNonceSize
,
envelope
.
Hash
())
}
if
envelope
.
PoW
()
<
w
h
.
MinPow
()
{
if
envelope
.
PoW
()
<
w
.
MinPow
()
{
log
.
Debug
(
"envelope with low PoW dropped"
,
"PoW"
,
envelope
.
PoW
(),
"hash"
,
envelope
.
Hash
()
.
Hex
())
return
false
,
nil
// drop envelope without error
}
hash
:=
envelope
.
Hash
()
w
h
.
poolMu
.
Lock
()
_
,
alreadyCached
:=
w
h
.
envelopes
[
hash
]
w
.
poolMu
.
Lock
()
_
,
alreadyCached
:=
w
.
envelopes
[
hash
]
if
!
alreadyCached
{
w
h
.
envelopes
[
hash
]
=
envelope
if
w
h
.
expirations
[
envelope
.
Expiry
]
==
nil
{
w
h
.
expirations
[
envelope
.
Expiry
]
=
set
.
NewNonTS
()
w
.
envelopes
[
hash
]
=
envelope
if
w
.
expirations
[
envelope
.
Expiry
]
==
nil
{
w
.
expirations
[
envelope
.
Expiry
]
=
set
.
NewNonTS
()
}
if
!
w
h
.
expirations
[
envelope
.
Expiry
]
.
Has
(
hash
)
{
w
h
.
expirations
[
envelope
.
Expiry
]
.
Add
(
hash
)
if
!
w
.
expirations
[
envelope
.
Expiry
]
.
Has
(
hash
)
{
w
.
expirations
[
envelope
.
Expiry
]
.
Add
(
hash
)
}
}
w
h
.
poolMu
.
Unlock
()
w
.
poolMu
.
Unlock
()
if
alreadyCached
{
log
.
Trace
(
"whisper envelope already cached"
,
"hash"
,
envelope
.
Hash
()
.
Hex
())
}
else
{
log
.
Trace
(
"cached whisper envelope"
,
"hash"
,
envelope
.
Hash
()
.
Hex
())
w
h
.
statsMu
.
Lock
()
w
h
.
stats
.
memoryUsed
+=
envelope
.
size
()
w
h
.
statsMu
.
Unlock
()
w
h
.
postEvent
(
envelope
,
false
)
// notify the local node about the new message
if
w
h
.
mailServer
!=
nil
{
w
h
.
mailServer
.
Archive
(
envelope
)
w
.
statsMu
.
Lock
()
w
.
stats
.
memoryUsed
+=
envelope
.
size
()
w
.
statsMu
.
Unlock
()
w
.
postEvent
(
envelope
,
false
)
// notify the local node about the new message
if
w
.
mailServer
!=
nil
{
w
.
mailServer
.
Archive
(
envelope
)
}
}
return
true
,
nil
...
...
@@ -838,9 +836,8 @@ func deriveKeyMaterial(key []byte, version uint64) (derivedKey []byte, err error
// because it's a once in a session experience
derivedKey
:=
pbkdf2
.
Key
(
key
,
nil
,
65356
,
aesKeyLength
,
sha256
.
New
)
return
derivedKey
,
nil
}
else
{
return
nil
,
unknownVersionError
(
version
)
}
return
nil
,
unknownVersionError
(
version
)
}
// GenerateRandomID generates a random string, which is then returned to be used as a key id
...
...
This diff is collapsed.
Click to expand it.
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