mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-17 01:13:45 +00:00
swarm/pss: Postal Service over Swarm
swarm/pss: Modular handshakes swarm/pss: Channel in client, incoming key expiry Remove unused msgC channel in client Handshake: incoming keys now have grace period swarm/pss: New network test (messages missing in transfer) swarm/pss: Track message counts per node, network test swarm/pss: Add message TTL swarm/pss: Remove simulation expect swarm/pss: Add random padding generation swarm/ swarm/add: Add 256 node snapshot swarm/pss: Add 128 node snapshot swarm/pss: Revert outside package edits swarm/pss: Add cli params to network test Revert to fmt.Errorf for formatted error strings Amend misc PR comments swarm/pss: Add valid pss peer check on send swarm/add: Add hash to topic generation swarm/pss: Introduct pss package topic + no handlerfail swarm/pss: Add missing forward on process fail swarm/pss: client fix Client implementation was broken after handshake changes: - Add missing api call GetAddress() - Use keyid instead of ecdsa.PublicKey in client - Move client handshake activation to inside client RunProtocol code - Simplify client handshake Also implements some lesser cosmetics: - Added build tag for exluding ping code - Added "pss" in build tags - Add conditional compile bool values - Simplify Ping Protocol code swarm/pss: API doc methods swarm/pss: API descriptions, moved to README README.md -> ARCHITECTURE.md API.md -> README.md swarm/pss: visual cleanup swarm/pss: visual edits swarm/pss: visual edits swarm/pss: Remove commented code swarm/pss: API param change omission (breaking) swarm/pss: Missing lock in topic hasher swarm/pss: Add api option of return msg data as hex swarm/pss: Revert new method, replace all payload with hexutil.Bytes swarm/pss: Remove redundant log msg swarm/pss: Change all topic, keys address to hex in API swarm/pss: Add JSON (un)marshal on topic and address swarm/pss: Updated README with new API descriptions swarm/pss: Check matching peer pss caps on forwarding swarm/pss: Use p2p.Cap.String() to build capstring swarm/pss: Simplify topic marshal + remove commented code swarm/pss: typo swarm/pss: Simply pssaddress marshal + 0x prefix bytestotopic Also added tests for topic conversions Changed topichex to topic in tests (and topic obj refs to topicobj) swarm/pss: Add slice comment, logline cleanup swarm/pss: comment cleanup
This commit is contained in:
parent
a8b626745d
commit
a12c003114
24 changed files with 4763 additions and 0 deletions
144
swarm/pss/ARCHITECTURE.md
Normal file
144
swarm/pss/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Postal Service over Swarm
|
||||||
|
|
||||||
|
Pss provides devp2p functionality for swarm nodes without the need for a direct tcp connection between them.
|
||||||
|
|
||||||
|
Messages are encapsulated in a devp2p message structure `PssMsg`. These capsules are forwarded from node to node using ordinary tcp devp2p until they reach their destination: The node or nodes who can successfully decrypt the message.
|
||||||
|
|
||||||
|
| Layer | Contents |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| PssMsg: | Address, Expiry |
|
||||||
|
| Envelope: | Topic |
|
||||||
|
| Payload: | e(data) |
|
||||||
|
|
||||||
|
Routing of messages is done using swarm's own kademlia routing. Optionally routing can be turned off, forcing the message to be sent to all peers, similar to the behavior of the whisper protocol.
|
||||||
|
|
||||||
|
Pss is intended for messages of limited size, typically a couple of Kbytes at most. The messages themselves can be anything at all; complex data structures or non-descript byte sequences.
|
||||||
|
|
||||||
|
For the current state and roadmap of pss development please see https://github.com/ethersphere/swarm/wiki/swarm-dev-progress.
|
||||||
|
|
||||||
|
Please report issues on https://github.com/ethersphere/go-ethereum
|
||||||
|
|
||||||
|
Feel free to ask questions in https://gitter.im/ethersphere/pss
|
||||||
|
|
||||||
|
## STATUS OF THIS DOCUMENT
|
||||||
|
|
||||||
|
`pss` is under active development, and the first implementation is yet to be merged to the Ethereum main branch. Expect things to change.
|
||||||
|
|
||||||
|
## CORE INTERFACES
|
||||||
|
|
||||||
|
The pss core provides low level control of key handling and message exchange.
|
||||||
|
|
||||||
|
### TOPICS
|
||||||
|
|
||||||
|
An encrypted envelope of a pss message always contains a Topic. This is pss' way of determining which message handlers to dispatch messages to. The topic of a message is only visible for the node(s) who can decrypt the message.
|
||||||
|
|
||||||
|
This "topic" is not like the subject of an email message, but a hash-like arbitrary 4 byte value. A valid topic can be generated using the `pss_*ToTopic` API methods.
|
||||||
|
|
||||||
|
### IDENTITY AND ENCRYPTION
|
||||||
|
|
||||||
|
Pss aims to achieve perfect darkness. That means that the minimum requirement for two nodes to communicate using pss is a shared secret. This secret can be an arbitrary byte slice, or a ECDSA keypair. The end recipient of a message is defined as the node that can successfully decrypt that message using stored keys.
|
||||||
|
|
||||||
|
A node's public key is derived from the private key passed to the `pss` constructor. Pss (currently) has no PKI.
|
||||||
|
|
||||||
|
Peer keys can manually be added to the pss node through its API calls `pss_setPeerPublicKey` and `pss_setSymmetricKey`. Keys are always coupled with a topic, and the keys will only be valid for these topics.
|
||||||
|
|
||||||
|
### CONNECTIONS
|
||||||
|
|
||||||
|
A "connection" in pss is a purely virtual construct. There is no mechanisms in place to ensure that the remote peer actually is there. In fact, "adding" a peer involves merely the node's opinion that the peer is there. It may issue messages to that remote peer to a directly connected peer, which in turn passes it on. But if it is not present on the network - or if there is no route to it - the message will never reach its destination through mere forwarding.
|
||||||
|
|
||||||
|
Since pss itself never requires a confirmation from a peer of whether a message is received or not, one could argue that pss shows `UDP`-like behavior.
|
||||||
|
|
||||||
|
It is also important to note that if the wrong (partial) address is set for a particular key/topic combination, the message may never reach that peer. The further left in the address byte slice the error lies, the less likely it is that delivery will occur.
|
||||||
|
|
||||||
|
|
||||||
|
### EXCHANGE
|
||||||
|
|
||||||
|
Message exchange in `pss` *requires* end-to-end encryption.
|
||||||
|
|
||||||
|
The API methods `pss_sendSym` and `pss_sendAsym` sends an arbitrary byte slice with a specific topic to a pss peer using the respective encryption scheme. The key passed to the send method must be associated with a topic in the pss key store prior to sending, or the send method will fail.
|
||||||
|
|
||||||
|
Return values from the send methods do *not* indicate whether the message was successfully delivered to the pss peer. It *only* indicates whether or not the message could be passed on to the network. If the message could not be forwarded to any peers, the method will fail.
|
||||||
|
|
||||||
|
Keep in mind that symmetric encryption is less resource-intensive than asymmetric encryption. The former should be used for nodes with high message volumes.
|
||||||
|
|
||||||
|
## EXTENSIONS
|
||||||
|
|
||||||
|
### HANDSHAKE
|
||||||
|
|
||||||
|
Pss offers an optional Diffie-Hellman handshake mechanism. Handshake functionality is activated per topic, and can be deactivated per topic even while the node is running.
|
||||||
|
|
||||||
|
Handshakes are activated in the code implementation of the node by running `SetHandshakeController()` on the pss node instance BEFORE starting the node service. The methods exposed by the HandshakeController's API gives the possibility to initiate, remove and check the state of handshakes and associated keys.
|
||||||
|
|
||||||
|
See the `HandshakeAPI` section in `godoc` for details.
|
||||||
|
|
||||||
|
### DEVP2P PROTOCOLS
|
||||||
|
|
||||||
|
The `Protocol` convenience structure is provided to mimic devp2p-type protocols over pss. In theory this makes it possible to reuse protocol code written for devp2p with a minimum of effort.
|
||||||
|
|
||||||
|
#### OUTGOING CONNECTIONS
|
||||||
|
|
||||||
|
In order to message a peer using this layer, a `Protocol` object must first be instantiated. When this is done, peers can be added using the protocol's `AddPeer()` method. The peer's key/topic combination must be in the pss key store before the peer can be aded.
|
||||||
|
|
||||||
|
Adding a peer in effect "runs" the protocol on that peer, and adds an internal mapping between a topic and that peer, and enables sending and receiving messages using the usual io-construct of devp2p. It does not actually *transmit* anything to the peer, it merely represents the node's opinion that a connection with the peer exists. (See CONNECTION above).
|
||||||
|
|
||||||
|
#### INCOMING CONNECTIONS
|
||||||
|
|
||||||
|
An incoming connection is nothing more than an actual PssMsg appearing with a certain Topic. If a Handler has been registered to that Topic, the message will be passed to it. This constitutes a "new" connection if:
|
||||||
|
|
||||||
|
- The pss node never called AddPeer with this combination of remote peer address and topic, and
|
||||||
|
|
||||||
|
- The pss node never received a PssMsg from this remote peer with this specific Topic before.
|
||||||
|
|
||||||
|
If it is a "new" connection, the protocol will be "run" on the remote peer, as if the peer was added via the API.
|
||||||
|
|
||||||
|
As with the `AddPeer()` method, the key/topic of the originating peer must exist in the pss key store.
|
||||||
|
|
||||||
|
#### TOPICS IN DEVP2P
|
||||||
|
|
||||||
|
The `ProtocolTopic()` method should be used to determine the correct topic to use for a pss `Protocol` instance.
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
Coming. Please refer to the tests for now.
|
||||||
|
|
||||||
|
## PSS INTERNALS
|
||||||
|
|
||||||
|
Pss implements the node.Service interface. It depends on a working kademlia overlay for routing.
|
||||||
|
|
||||||
|
### DECRYPTION
|
||||||
|
|
||||||
|
When processing an incoming message, `pss` detects whether it is encrypted symmetrically or asymmetrically.
|
||||||
|
|
||||||
|
When decrypting symmetrically, `pss` iterates through all stored keys, and attempts to decrypt with each key in order.
|
||||||
|
|
||||||
|
pss keeps a *cache* of these keys. The cache will only store a certain amount of keys, and the iterator will return keys in the order of most recently used key first. Abandoned keys will be garbage collected.
|
||||||
|
|
||||||
|
### ROUTING
|
||||||
|
|
||||||
|
(please refer to swarm kademlia routing for an explanation of the routing algorithm used for pss)
|
||||||
|
|
||||||
|
`pss` uses *address hinting* for routing. The address hint is an arbitrary-length MSB byte slice of the peer's swarm overlay address. It can be the whole address, part of the address, or even an empty byte slice. The slice will be matched to the MSB slice of the same length of all devp2p peers in the routing stage.
|
||||||
|
|
||||||
|
If an empty byte slice is passed, all devp2p peers will match the address hint, and the message will be forwarded to everyone. This is equivalent to `whisper` routing, and makes it difficult to perform traffic analysis based on who messages are forwarded to.
|
||||||
|
|
||||||
|
A node will also forward to everyone if the address hint provided is in its proximity bin, both to provide saturation to increase chances of delivery, and also for recipient obfuscation to thwart traffic analysis attacks. The recipient node(s) will always forward to all its peers.
|
||||||
|
|
||||||
|
### CACHING
|
||||||
|
|
||||||
|
pss implements a simple caching mechanism for messages, using the swarm DPA for storage of the messages and generation of the digest keys used in the cache table. The caching is intended to alleviate the following:
|
||||||
|
|
||||||
|
- save messages so that they can be delivered later if the recipient was not online at the time of sending.
|
||||||
|
|
||||||
|
- drop an identical message to the same recipient if received within a given time interval
|
||||||
|
|
||||||
|
- prevent backwards routing of messages
|
||||||
|
|
||||||
|
the latter may occur if only one entry is in the receiving node's kademlia, or if the proximity of the current node recipient hinted by the address is so close that the message will be forwarded to everyone. In these cases the forwarder will be provided as the "nearest node" to the final recipient. The cache keeps the address of who the message was forwarded from, and if the cache lookup matches, the message will be dropped.
|
||||||
|
|
||||||
|
### DEVP2P PROTOCOLS
|
||||||
|
|
||||||
|
When implementing devp2p protocols, topics are derived from protocols' name and version. The Protocol provides a generic Handler that be passed to Pss.Register. This makes it possible to use the same message handler code for pss that is used for directly connected peers in devp2p.
|
||||||
|
|
||||||
|
Under the hood, pss implements its own MsgReadWriter, which bridges MsgReadWriter.WriteMsg with Pss.SendRaw, and deftly adds an InjectMsg method which pipes incoming messages to appear on the MsgReadWriter.ReadMsg channel.
|
||||||
|
|
||||||
|
|
||||||
318
swarm/pss/README.md
Normal file
318
swarm/pss/README.md
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
# Postal Services over Swarm
|
||||||
|
|
||||||
|
`pss` enables message relay over swarm. This means nodes can send messages to each other without being directly connected with each other, while taking advantage of the efficient routing algorithms that swarm uses for transporting and storing data.
|
||||||
|
|
||||||
|
### CONTENTS
|
||||||
|
|
||||||
|
* Status of this document
|
||||||
|
* Core concepts
|
||||||
|
* Caveat
|
||||||
|
* Examples
|
||||||
|
* API
|
||||||
|
* Retrieve node information
|
||||||
|
* Receive messages
|
||||||
|
* Send messages using public key encryption
|
||||||
|
* Send messages using symmetric encryption
|
||||||
|
* Querying peer keys
|
||||||
|
* Handshakes
|
||||||
|
|
||||||
|
### STATUS OF THIS DOCUMENT
|
||||||
|
|
||||||
|
`pss` is under active development, and the first implementation is yet to be merged to the Ethereum main branch. Expect things to change.
|
||||||
|
|
||||||
|
Details on swarm routing and encryption schemes out of scope of this document.
|
||||||
|
|
||||||
|
Please refer to [ARCHITECTURE.md](ARCHITECTURE.md) for in-depth topics concerning `pss`.
|
||||||
|
|
||||||
|
## CORE CONCEPTS
|
||||||
|
|
||||||
|
Three things are required to send a `pss` message:
|
||||||
|
|
||||||
|
1. Encryption key
|
||||||
|
2. Topic
|
||||||
|
3. Message payload
|
||||||
|
|
||||||
|
Encryption key can be a public key or a 32 byte symmetric key. It must be coupled with a peer address in the node prior to sending.
|
||||||
|
|
||||||
|
Topic is the initial 4 bytes of a hash value.
|
||||||
|
|
||||||
|
Message payload is an arbitrary byte slice of data.
|
||||||
|
|
||||||
|
Upon sending the message it is encrypted and passed on from peer to peer. Any node along the route that can successfully decrypt the message is regarded as a recipient. Recipients continue to pass on the message to their peers, to make traffic analysis attacks more difficult.
|
||||||
|
|
||||||
|
The Address that is coupled with the encryption keys are used for routing the message. This does *not* need to be a full addresses; the network will route the message to the best of its ability with the information that is available. If *no* address is given (zero-length byte slice), routing is effectively deactivated, and the message is passed to all peers by all peers.
|
||||||
|
|
||||||
|
## CAVEAT
|
||||||
|
|
||||||
|
`pss` connectivity resembles UDP. This means there is no delivery guarantee for a message. Furthermore there is no strict definition of what a connection between two nodes communicating via `pss` is. Reception acknowledgements and keepalive-schemes is the responsibility of the application.
|
||||||
|
|
||||||
|
Due to the inherent properties of the `swarm` routing algorithm, a node may receive the same message more than once. Message deduplication *cannot be guaranteed* by `pss`, and must be handled in the application layer to ensure predictable results.
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
The code tutorial [p2p programming in go-ethereum](https://github.com/nolash/go-ethereum-p2p-demo) by [@nolash](https://github.com/nolash) provides step-by-step code examples for usage of `pss` API with `go-ethereum` nodes.
|
||||||
|
|
||||||
|
A quite unpolished example using `javascript` is available here: [https://github.com/nolash/pss-js/tree/withcrypt](https://github.com/nolash/pss-js/tree/withcrypt)
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The `pss` API is available through IPC and Websockets. There is currently no `web3.js` implementation, as this does not support message subscription.
|
||||||
|
|
||||||
|
For `golang` clients, please use the `rpc.Client` provided by the `go-ethereum` repository. The return values may have special types in `golang`. Please refer to `godoc` for details.
|
||||||
|
|
||||||
|
### RETRIEVE NODE INFORMATION
|
||||||
|
|
||||||
|
#### pss_getPublicKey
|
||||||
|
|
||||||
|
Retrieves the public key of the node, in hex format
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
none
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. publickey (hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_baseAddr
|
||||||
|
|
||||||
|
Retrieves the swarm overlay address of the node, in hex format
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
none
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. swarm overlay address (hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_stringToTopic
|
||||||
|
|
||||||
|
Creates a deterministic 4 byte topic value from input, returned in hex format
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. topic string (string)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. pss topic (hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
### RECEIVE MESSAGES
|
||||||
|
|
||||||
|
#### pss_subscribe
|
||||||
|
|
||||||
|
Creates a subscription. Received messages with matching topic will be passed to subscription client.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. string("receive")
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. subscription handle `base64(byte)` `rpc.ClientSubscription`
|
||||||
|
```
|
||||||
|
|
||||||
|
In `golang` as special method is used:
|
||||||
|
|
||||||
|
`rpc.Client.Subscribe(context.Context, "pss", chan pss.APIMsg, "receive", pss.Topic)`
|
||||||
|
|
||||||
|
Incoming messages are encapsulated in an object (`pss.APIMsg` in `golang`) with the following members:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Msg (hex) - the message payload
|
||||||
|
2. Asymmetric (bool) - true if message used public key encryption
|
||||||
|
3. Key (string) - the encryption key used
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEND MESSAGE USING PUBLIC KEY ENCRYPTION
|
||||||
|
|
||||||
|
#### pss_setPeerPublicKey
|
||||||
|
|
||||||
|
Register a peer's public key. This is done once for every topic that will be used with the peer. Address can be anything from 0 to 32 bytes inclusive of the peer's swarm overlay address.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. public key of peer (hex)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. address of peer (hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
none
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_sendAsym
|
||||||
|
|
||||||
|
Encrypts the message using the provided public key, and signs it using the node's private key. It then wraps it in an envelope containing the topic, and sends it to the network.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. public key of peer (hex)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. message (hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
none
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEND MESSAGE USING SYMMETRIC ENCRYPTION
|
||||||
|
|
||||||
|
#### pss_setSymmetricKey
|
||||||
|
|
||||||
|
Register a symmetric key shared with a peer. This is done once for every topic that will be used with the peer. Address can be anything from 0 to 32 bytes inclusive of the peer's swarm overlay address.
|
||||||
|
|
||||||
|
If the fourth parameter is false, the key will *not* be added to the list of symmetric keys used for decryption attempts.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. symmetric key (hex)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. address of peer (hex)
|
||||||
|
4. use for decryption (bool)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. symmetric key id (string)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_sendSym
|
||||||
|
|
||||||
|
Encrypts the message using the provided symmetric key, wraps it in an envelope containing the topic, and sends it to the network.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. symmetric key id (string)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. message (hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
none
|
||||||
|
```
|
||||||
|
|
||||||
|
### QUERY PEER KEYS
|
||||||
|
|
||||||
|
#### pss_GetSymmetricAddressHint
|
||||||
|
|
||||||
|
Return the swarm overlay address associated with the peer registered with the given symmetric key and topic combination.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. topic (4 bytes in hex)
|
||||||
|
2. symmetric key id (string)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. peer address (hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_GetAsymmetricAddressHint
|
||||||
|
|
||||||
|
Return the swarm overlay address associated with the peer registered with the given symmetric key and topic combination.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. topic (4 bytes in hex)
|
||||||
|
2. public key in hex form (string)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. peer address (hex)
|
||||||
|
```
|
||||||
|
|
||||||
|
### HANDSHAKES
|
||||||
|
|
||||||
|
Convenience implementation of Diffie-Hellman handshakes using ephemeral symmetric keys. Peers keep separate sets of keys for incoming and outgoing communications.
|
||||||
|
|
||||||
|
*This functionality is an optional feature in `pss`. It is compiled in by default, but can be omitted by providing the `nopsshandshake` build tag.*
|
||||||
|
|
||||||
|
#### pss_addHandshake
|
||||||
|
|
||||||
|
Activate handshake functionality on the specified topic.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. topic (4 bytes in hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
none
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_removeHandshake
|
||||||
|
|
||||||
|
Remove handshake functionality on the specified topic.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. topic (4 bytes in hex)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
none
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_handshake
|
||||||
|
|
||||||
|
Instantiate handshake with peer, refreshing symmetric encryption keys.
|
||||||
|
|
||||||
|
If parameter 3 is false, the returned array will be empty.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. public key of peer in hex format (string)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. block calls until keys are received (bool)
|
||||||
|
4. flush existing incoming keys (bool)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. list of symmetric keys (string[])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_getHandshakeKeys
|
||||||
|
|
||||||
|
Get valid symmetric encryption keys for a specified peer and topic.
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
1. public key of peer in hex format (string)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. include keys for incoming messages (bool)
|
||||||
|
4. include keys for outgoing messages (bool)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. list of symmetric keys (string[])
|
||||||
|
|
||||||
|
#### pss_getHandshakeKeyCapacity
|
||||||
|
|
||||||
|
Get amount of remaining messages the specified key is valid for.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. symmetric key id (string)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. number of messages (uint16)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_getHandshakePublicKey
|
||||||
|
|
||||||
|
Get the peer's public key associated with the specified symmetric key.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. symmetric key id (string)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. Associated public key in hex format (string)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pss_releaseHandshakeKey
|
||||||
|
|
||||||
|
Invalidate the specified key.
|
||||||
|
|
||||||
|
Normally, the key will be kept for a grace period to allow for decryption of delayed messages. If instant removal is set, this grace period is omitted, and the key removed instantaneously.
|
||||||
|
|
||||||
|
```
|
||||||
|
parameters:
|
||||||
|
1. public key of peer in hex format (string)
|
||||||
|
2. topic (4 bytes in hex)
|
||||||
|
3. symmetric key id to release (string)
|
||||||
|
4. remove keys instantly (bool)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
1. whether key was successfully removed (bool)
|
||||||
|
```
|
||||||
134
swarm/pss/api.go
Normal file
134
swarm/pss/api.go
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrapper for receiving pss messages when using the pss API
|
||||||
|
// providing access to sender of message
|
||||||
|
type APIMsg struct {
|
||||||
|
Msg hexutil.Bytes
|
||||||
|
Asymmetric bool
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional public methods accessible through API for pss
|
||||||
|
type API struct {
|
||||||
|
*Pss
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPI(ps *Pss) *API {
|
||||||
|
return &API{Pss: ps}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new subscription for the caller. Enables external handling of incoming messages.
|
||||||
|
//
|
||||||
|
// A new handler is registered in pss for the supplied topic
|
||||||
|
//
|
||||||
|
// All incoming messages to the node matching this topic will be encapsulated in the APIMsg
|
||||||
|
// struct and sent to the subscriber
|
||||||
|
func (pssapi *API) Receive(ctx context.Context, topic Topic) (*rpc.Subscription, error) {
|
||||||
|
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||||
|
if !supported {
|
||||||
|
return nil, fmt.Errorf("Subscribe not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
psssub := notifier.CreateSubscription()
|
||||||
|
|
||||||
|
handler := func(msg []byte, p *p2p.Peer, asymmetric bool, keyid string) error {
|
||||||
|
apimsg := &APIMsg{
|
||||||
|
Msg: hexutil.Bytes(msg),
|
||||||
|
Asymmetric: asymmetric,
|
||||||
|
Key: keyid,
|
||||||
|
}
|
||||||
|
if err := notifier.Notify(psssub.ID, apimsg); err != nil {
|
||||||
|
log.Warn(fmt.Sprintf("notification on pss sub topic rpc (sub %v) msg %v failed!", psssub.ID, msg))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deregf := pssapi.Register(&topic, handler)
|
||||||
|
go func() {
|
||||||
|
defer deregf()
|
||||||
|
select {
|
||||||
|
case err := <-psssub.Err():
|
||||||
|
log.Warn(fmt.Sprintf("caught subscription error in pss sub topic %x: %v", topic, err))
|
||||||
|
case <-notifier.Closed():
|
||||||
|
log.Warn(fmt.Sprintf("rpc sub notifier closed"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return psssub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) GetAddress(topic Topic, asymmetric bool, key string) (PssAddress, error) {
|
||||||
|
var addr *PssAddress
|
||||||
|
if asymmetric {
|
||||||
|
peer, ok := pssapi.Pss.pubKeyPool[key][topic]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("pubkey/topic pair %x/%x doesn't exist", key, topic)
|
||||||
|
}
|
||||||
|
addr = peer.address
|
||||||
|
} else {
|
||||||
|
peer, ok := pssapi.Pss.symKeyPool[key][topic]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("symkey/topic pair %x/%x doesn't exist", key, topic)
|
||||||
|
}
|
||||||
|
addr = peer.address
|
||||||
|
|
||||||
|
}
|
||||||
|
return *addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the node's base address in hex form
|
||||||
|
func (pssapi *API) BaseAddr() (PssAddress, error) {
|
||||||
|
return PssAddress(pssapi.Pss.BaseAddr()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the node's public key in hex form
|
||||||
|
func (pssapi *API) GetPublicKey() (keybytes hexutil.Bytes) {
|
||||||
|
key := pssapi.Pss.PublicKey()
|
||||||
|
keybytes = crypto.FromECDSAPub(key)
|
||||||
|
return hexutil.Bytes(keybytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Public key to associate with a particular Pss peer
|
||||||
|
func (pssapi *API) SetPeerPublicKey(pubkey hexutil.Bytes, topic Topic, addr PssAddress) error {
|
||||||
|
err := pssapi.Pss.SetPeerPublicKey(crypto.ToECDSAPub(pubkey), topic, &addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid key: %x", pubkey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) GetSymmetricKey(symkeyid string) (hexutil.Bytes, error) {
|
||||||
|
symkey, err := pssapi.Pss.GetSymmetricKey(symkeyid)
|
||||||
|
return hexutil.Bytes(symkey), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) GetSymmetricAddressHint(topic Topic, symkeyid string) (PssAddress, error) {
|
||||||
|
return *pssapi.Pss.symKeyPool[symkeyid][topic].address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) GetAsymmetricAddressHint(topic Topic, pubkeyid string) (PssAddress, error) {
|
||||||
|
return *pssapi.Pss.pubKeyPool[pubkeyid][topic].address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) StringToTopic(topicstring string) (Topic, error) {
|
||||||
|
return BytesToTopic([]byte(topicstring)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) SendAsym(pubkeyhex string, topic Topic, msg hexutil.Bytes) error {
|
||||||
|
return pssapi.Pss.SendAsym(pubkeyhex, topic, msg[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pssapi *API) SendSym(symkeyhex string, topic Topic, msg hexutil.Bytes) error {
|
||||||
|
return pssapi.Pss.SendSym(symkeyhex, topic, msg[:])
|
||||||
|
}
|
||||||
328
swarm/pss/client/client.go
Normal file
328
swarm/pss/client/client.go
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
// +build !noclient,!noprotocol
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/protocols"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/pss"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
handshakeRetryTimeout = 1000
|
||||||
|
handshakeRetryCount = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// The pss client provides devp2p emulation over pss RPC API,
|
||||||
|
// giving access to pss methods from a different process
|
||||||
|
type Client struct {
|
||||||
|
BaseAddrHex string
|
||||||
|
|
||||||
|
// peers
|
||||||
|
peerPool map[pss.Topic]map[string]*pssRPCRW
|
||||||
|
protos map[pss.Topic]*p2p.Protocol
|
||||||
|
|
||||||
|
// rpc connections
|
||||||
|
rpc *rpc.Client
|
||||||
|
subs []*rpc.ClientSubscription
|
||||||
|
|
||||||
|
// channels
|
||||||
|
topicsC chan []byte
|
||||||
|
quitC chan struct{}
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements p2p.MsgReadWriter
|
||||||
|
type pssRPCRW struct {
|
||||||
|
*Client
|
||||||
|
topic string
|
||||||
|
msgC chan []byte
|
||||||
|
addr pss.PssAddress
|
||||||
|
pubKeyId string
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Client) newpssRPCRW(pubkeyid string, addr pss.PssAddress, topicobj pss.Topic) (*pssRPCRW, error) {
|
||||||
|
topic := topicobj.String()
|
||||||
|
err := self.rpc.Call(nil, "pss_setPeerPublicKey", pubkeyid, topic, hexutil.Encode(addr[:]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("setpeer %s %s: %v", topic, pubkeyid, err)
|
||||||
|
}
|
||||||
|
return &pssRPCRW{
|
||||||
|
Client: self,
|
||||||
|
topic: topic,
|
||||||
|
msgC: make(chan []byte),
|
||||||
|
addr: addr,
|
||||||
|
pubKeyId: pubkeyid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *pssRPCRW) ReadMsg() (p2p.Msg, error) {
|
||||||
|
msg := <-rw.msgC
|
||||||
|
log.Trace("pssrpcrw read", "msg", msg)
|
||||||
|
pmsg, err := pss.ToP2pMsg(msg)
|
||||||
|
if err != nil {
|
||||||
|
return p2p.Msg{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pmsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one message slot left
|
||||||
|
// then new is requested through handshake
|
||||||
|
// if buffer is empty, handshake request blocks until return
|
||||||
|
// after which pointer is changed to first new key in buffer
|
||||||
|
// will fail if:
|
||||||
|
// - any api calls fail
|
||||||
|
// - handshake retries are exhausted without reply,
|
||||||
|
// - send fails
|
||||||
|
func (rw *pssRPCRW) WriteMsg(msg p2p.Msg) error {
|
||||||
|
log.Trace("got writemsg pssclient", "msg", msg)
|
||||||
|
rlpdata := make([]byte, msg.Size)
|
||||||
|
msg.Payload.Read(rlpdata)
|
||||||
|
pmsg, err := rlp.EncodeToBytes(pss.ProtocolMsg{
|
||||||
|
Code: msg.Code,
|
||||||
|
Size: msg.Size,
|
||||||
|
Payload: rlpdata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the keys we have
|
||||||
|
var symkeyids []string
|
||||||
|
err = rw.Client.rpc.Call(&symkeyids, "pss_getHandshakeKeys", rw.pubKeyId, rw.topic, false, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the capacity of the first key
|
||||||
|
var symkeycap uint16
|
||||||
|
if len(symkeyids) > 0 {
|
||||||
|
err = rw.Client.rpc.Call(&symkeycap, "pss_getHandshakeKeyCapacity", symkeyids[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rw.Client.rpc.Call(nil, "pss_sendSym", symkeyids[0], rw.topic, hexutil.Encode(pmsg))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the last message it is valid for, initiate new handshake
|
||||||
|
if symkeycap == 1 {
|
||||||
|
var retries int
|
||||||
|
var sync bool
|
||||||
|
// if it's the only remaining key, make sure we don't continue until we have new ones for further writes
|
||||||
|
if len(symkeyids) == 1 {
|
||||||
|
sync = true
|
||||||
|
}
|
||||||
|
// initiate handshake
|
||||||
|
_, err := rw.handshake(retries, sync, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("failing", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// retry and synchronicity wrapper for handshake api call
|
||||||
|
// returns first new symkeyid upon successful execution
|
||||||
|
func (rw *pssRPCRW) handshake(retries int, sync bool, flush bool) (string, error) {
|
||||||
|
|
||||||
|
var symkeyids []string
|
||||||
|
var i int
|
||||||
|
// request new keys
|
||||||
|
// if the key buffer was depleted, make this as a blocking call and try several times before giving up
|
||||||
|
for i = 0; i < 1+retries; i++ {
|
||||||
|
log.Debug("handshake attempt pssrpcrw", "pubkeyid", rw.pubKeyId, "topic", rw.topic, "sync", sync)
|
||||||
|
err := rw.Client.rpc.Call(&symkeyids, "pss_handshake", rw.pubKeyId, rw.topic, sync, flush)
|
||||||
|
if err == nil {
|
||||||
|
var keyid string
|
||||||
|
if sync {
|
||||||
|
keyid = symkeyids[0]
|
||||||
|
}
|
||||||
|
return keyid, nil
|
||||||
|
}
|
||||||
|
if i-1+retries > 1 {
|
||||||
|
time.Sleep(time.Millisecond * handshakeRetryTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("handshake failed after %d attempts", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom constructor
|
||||||
|
//
|
||||||
|
// Provides direct access to the rpc object
|
||||||
|
func NewClient(rpcurl string) (*Client, error) {
|
||||||
|
rpcclient, err := rpc.Dial(rpcurl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewClientWithRPC(rpcclient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main constructor
|
||||||
|
//
|
||||||
|
// The 'rpcclient' parameter allows passing a in-memory rpc client to act as the remote websocket RPC.
|
||||||
|
func NewClientWithRPC(rpcclient *rpc.Client) (*Client, error) {
|
||||||
|
client := newClient()
|
||||||
|
client.rpc = rpcclient
|
||||||
|
err := client.rpc.Call(&client.BaseAddrHex, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get pss node baseaddress: %v", err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient() (client *Client) {
|
||||||
|
client = &Client{
|
||||||
|
quitC: make(chan struct{}),
|
||||||
|
peerPool: make(map[pss.Topic]map[string]*pssRPCRW),
|
||||||
|
protos: make(map[pss.Topic]*p2p.Protocol),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mounts a new devp2p protcool on the pss connection
|
||||||
|
//
|
||||||
|
// the protocol is aliased as a "pss topic"
|
||||||
|
// uses normal devp2p send and incoming message handler routines from the p2p/protocols package
|
||||||
|
//
|
||||||
|
// when an incoming message is received from a peer that is not yet known to the client,
|
||||||
|
// this peer object is instantiated, and the protocol is run on it.
|
||||||
|
func (self *Client) RunProtocol(ctx context.Context, proto *p2p.Protocol) error {
|
||||||
|
topicobj := pss.BytesToTopic([]byte(fmt.Sprintf("%s:%d", proto.Name, proto.Version)))
|
||||||
|
topichex := topicobj.String()
|
||||||
|
msgC := make(chan pss.APIMsg)
|
||||||
|
self.peerPool[topicobj] = make(map[string]*pssRPCRW)
|
||||||
|
sub, err := self.rpc.Subscribe(ctx, "pss", msgC, "receive", topichex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pss event subscription failed: %v", err)
|
||||||
|
}
|
||||||
|
self.subs = append(self.subs, sub)
|
||||||
|
err = self.rpc.Call(nil, "pss_addHandshake", topichex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pss handshake activation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch incoming messages
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-msgC:
|
||||||
|
// we only allow sym msgs here
|
||||||
|
if msg.Asymmetric {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// we get passed the symkeyid
|
||||||
|
// need the symkey itself to resolve to peer's pubkey
|
||||||
|
var pubkeyid string
|
||||||
|
err = self.rpc.Call(&pubkeyid, "pss_getHandshakePublicKey", msg.Key)
|
||||||
|
if err != nil || pubkeyid == "" {
|
||||||
|
log.Trace("proto err or no pubkey", "err", err, "symkeyid", msg.Key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// if we don't have the peer on this protocol already, create it
|
||||||
|
// this is more or less the same as AddPssPeer, less the handshake initiation
|
||||||
|
if self.peerPool[topicobj][pubkeyid] == nil {
|
||||||
|
var addrhex string
|
||||||
|
err := self.rpc.Call(&addrhex, "pss_getAddress", topichex, false, msg.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Trace(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrbytes, err := hexutil.Decode(addrhex)
|
||||||
|
if err != nil {
|
||||||
|
log.Trace(err.Error())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := pss.PssAddress(addrbytes)
|
||||||
|
rw, err := self.newpssRPCRW(pubkeyid, addr, topicobj)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
self.peerPool[topicobj][pubkeyid] = rw
|
||||||
|
nid, _ := discover.HexID("0x00")
|
||||||
|
p := p2p.NewPeer(nid, fmt.Sprintf("%v", addr), []p2p.Cap{})
|
||||||
|
go proto.Run(p, self.peerPool[topicobj][pubkeyid])
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
self.peerPool[topicobj][pubkeyid].msgC <- msg.Msg
|
||||||
|
}()
|
||||||
|
case <-self.quitC:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
self.protos[topicobj] = proto
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always call this to ensure that we exit cleanly
|
||||||
|
func (self *Client) Close() error {
|
||||||
|
for _, s := range self.subs {
|
||||||
|
s.Unsubscribe()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a pss peer (public key) and run the protocol on it
|
||||||
|
//
|
||||||
|
// client.RunProtocol with matching topic must have been
|
||||||
|
// run prior to adding the peer, or this method will
|
||||||
|
// return an error.
|
||||||
|
//
|
||||||
|
// The key must exist in the key store of the pss node
|
||||||
|
// before the peer is added. The method will return an error
|
||||||
|
// if it is not.
|
||||||
|
func (self *Client) AddPssPeer(pubkeyid string, addr []byte, spec *protocols.Spec) error {
|
||||||
|
topic := pss.ProtocolTopic(spec)
|
||||||
|
if self.peerPool[topic] == nil {
|
||||||
|
return errors.New("addpeer on unset topic")
|
||||||
|
}
|
||||||
|
if self.peerPool[topic][pubkeyid] == nil {
|
||||||
|
rw, err := self.newpssRPCRW(pubkeyid, addr, topic)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = rw.handshake(handshakeRetryCount, true, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
self.peerPool[topic][pubkeyid] = rw
|
||||||
|
nid, _ := discover.HexID("0x00")
|
||||||
|
p := p2p.NewPeer(nid, fmt.Sprintf("%v", addr), []p2p.Cap{})
|
||||||
|
go self.protos[topic].Run(p, self.peerPool[topic][pubkeyid])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a pss peer
|
||||||
|
//
|
||||||
|
// TODO: underlying cleanup
|
||||||
|
func (self *Client) RemovePssPeer(pubkeyid string, spec *protocols.Spec) {
|
||||||
|
topic := pss.ProtocolTopic(spec)
|
||||||
|
delete(self.peerPool[topic], pubkeyid)
|
||||||
|
}
|
||||||
285
swarm/pss/client/client_test.go
Normal file
285
swarm/pss/client/client_test.go
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/node"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/simulations"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/network"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/pss"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pssServiceName = "pss"
|
||||||
|
bzzServiceName = "bzz"
|
||||||
|
)
|
||||||
|
|
||||||
|
type protoCtrl struct {
|
||||||
|
C chan bool
|
||||||
|
protocol *pss.Protocol
|
||||||
|
run func(*p2p.Peer, p2p.MsgReadWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
debugdebugflag = flag.Bool("vv", false, "veryverbose")
|
||||||
|
debugflag = flag.Bool("v", false, "verbose")
|
||||||
|
w *whisper.Whisper
|
||||||
|
wapi *whisper.PublicWhisperAPI
|
||||||
|
// custom logging
|
||||||
|
psslogmain log.Logger
|
||||||
|
pssprotocols map[string]*protoCtrl
|
||||||
|
sendLimit = uint16(256)
|
||||||
|
)
|
||||||
|
|
||||||
|
var services = newServices()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Parse()
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
|
adapters.RegisterServices(services)
|
||||||
|
|
||||||
|
loglevel := log.LvlInfo
|
||||||
|
if *debugflag {
|
||||||
|
loglevel = log.LvlDebug
|
||||||
|
} else if *debugdebugflag {
|
||||||
|
loglevel = log.LvlTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
psslogmain = log.New("psslog", "*")
|
||||||
|
hs := log.StreamHandler(os.Stderr, log.TerminalFormat(true))
|
||||||
|
hf := log.LvlFilterHandler(loglevel, hs)
|
||||||
|
h := log.CallerFileHandler(hf)
|
||||||
|
log.Root().SetHandler(h)
|
||||||
|
|
||||||
|
w = whisper.New(&whisper.DefaultConfig)
|
||||||
|
wapi = whisper.NewPublicWhisperAPI(w)
|
||||||
|
|
||||||
|
pssprotocols = make(map[string]*protoCtrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ping pong exchange across one expired symkey
|
||||||
|
func TestClientHandshake(t *testing.T) {
|
||||||
|
sendLimit = 3
|
||||||
|
|
||||||
|
clients, err := setupNetwork(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lpsc, err := NewClientWithRPC(clients[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rpsc, err := NewClientWithRPC(clients[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lpssping := &pss.Ping{
|
||||||
|
OutC: make(chan bool),
|
||||||
|
InC: make(chan bool),
|
||||||
|
Pong: false,
|
||||||
|
}
|
||||||
|
rpssping := &pss.Ping{
|
||||||
|
OutC: make(chan bool),
|
||||||
|
InC: make(chan bool),
|
||||||
|
Pong: false,
|
||||||
|
}
|
||||||
|
lproto := pss.NewPingProtocol(lpssping)
|
||||||
|
rproto := pss.NewPingProtocol(rpssping)
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
err = lpsc.RunProtocol(ctx, lproto)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = rpsc.RunProtocol(ctx, rproto)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
topic := pss.PingTopic.String()
|
||||||
|
|
||||||
|
var loaddr string
|
||||||
|
err = clients[0].Call(&loaddr, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
var roaddr string
|
||||||
|
err = clients[1].Call(&roaddr, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lpubkey string
|
||||||
|
err = clients[0].Call(&lpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
var rpubkey string
|
||||||
|
err = clients[1].Call(&rpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clients[0].Call(nil, "pss_setPeerPublicKey", rpubkey, topic, roaddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = clients[1].Call(nil, "pss_setPeerPublicKey", lpubkey, topic, loaddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
roaddrbytes, err := hexutil.Decode(roaddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = lpsc.AddPssPeer(rpubkey, roaddrbytes, pss.PingProtocol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
for i := uint16(0); i <= sendLimit; i++ {
|
||||||
|
lpssping.OutC <- false
|
||||||
|
got := <-rpssping.InC
|
||||||
|
log.Warn("ok", "idx", i, "got", got)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupNetwork(numnodes int) (clients []*rpc.Client, err error) {
|
||||||
|
nodes := make([]*simulations.Node, numnodes)
|
||||||
|
clients = make([]*rpc.Client, numnodes)
|
||||||
|
if numnodes < 2 {
|
||||||
|
return nil, fmt.Errorf("Minimum two nodes in network")
|
||||||
|
}
|
||||||
|
adapter := adapters.NewSimAdapter(services)
|
||||||
|
net := simulations.NewNetwork(adapter, &simulations.NetworkConfig{
|
||||||
|
ID: "0",
|
||||||
|
DefaultService: "bzz",
|
||||||
|
})
|
||||||
|
for i := 0; i < numnodes; i++ {
|
||||||
|
nodes[i], err = net.NewNodeWithConfig(&adapters.NodeConfig{
|
||||||
|
Services: []string{"bzz", "pss"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating node 1: %v", err)
|
||||||
|
}
|
||||||
|
err = net.Start(nodes[i].ID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error starting node 1: %v", err)
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
err = net.Connect(nodes[i].ID(), nodes[i-1].ID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error connecting nodes: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clients[i], err = nodes[i].Client()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create node 1 rpc client fail: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if numnodes > 2 {
|
||||||
|
err = net.Connect(nodes[0].ID(), nodes[len(nodes)-1].ID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error connecting first and last nodes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServices() adapters.Services {
|
||||||
|
stateStore := newTestStore()
|
||||||
|
kademlias := make(map[discover.NodeID]*network.Kademlia)
|
||||||
|
kademlia := func(id discover.NodeID) *network.Kademlia {
|
||||||
|
if k, ok := kademlias[id]; ok {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
addr := network.NewAddrFromNodeID(id)
|
||||||
|
params := network.NewKadParams()
|
||||||
|
params.MinProxBinSize = 2
|
||||||
|
params.MaxBinSize = 3
|
||||||
|
params.MinBinSize = 1
|
||||||
|
params.MaxRetries = 1000
|
||||||
|
params.RetryExponent = 2
|
||||||
|
params.RetryInterval = 1000000
|
||||||
|
kademlias[id] = network.NewKademlia(addr.Over(), params)
|
||||||
|
return kademlias[id]
|
||||||
|
}
|
||||||
|
return adapters.Services{
|
||||||
|
"pss": func(ctx *adapters.ServiceContext) (node.Service, error) {
|
||||||
|
cachedir, err := ioutil.TempDir("", "pss-cache")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create pss cache tmpdir failed", "error", err)
|
||||||
|
}
|
||||||
|
dpa, err := storage.NewLocalDPA(cachedir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("local dpa creation failed", "error", err)
|
||||||
|
}
|
||||||
|
ctxlocal, _ := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
keys, err := wapi.NewKeyPair(ctxlocal)
|
||||||
|
privkey, err := w.GetPrivateKey(keys)
|
||||||
|
psparams := pss.NewPssParams(privkey)
|
||||||
|
pskad := kademlia(ctx.Config.ID)
|
||||||
|
ps := pss.NewPss(pskad, dpa, psparams)
|
||||||
|
pshparams := pss.NewHandshakeParams()
|
||||||
|
pshparams.SymKeySendLimit = sendLimit
|
||||||
|
err = pss.SetHandshakeController(ps, pshparams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("handshake controller fail: %v", err)
|
||||||
|
}
|
||||||
|
return ps, nil
|
||||||
|
},
|
||||||
|
"bzz": func(ctx *adapters.ServiceContext) (node.Service, error) {
|
||||||
|
addr := network.NewAddrFromNodeID(ctx.Config.ID)
|
||||||
|
hp := network.NewHiveParams()
|
||||||
|
hp.Discovery = false
|
||||||
|
config := &network.BzzConfig{
|
||||||
|
OverlayAddr: addr.Over(),
|
||||||
|
UnderlayAddr: addr.Under(),
|
||||||
|
HiveParams: hp,
|
||||||
|
}
|
||||||
|
return network.NewBzz(config, kademlia(ctx.Config.ID), stateStore), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from swarm/network/protocol_test_go
|
||||||
|
type testStore struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
values map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestStore() *testStore {
|
||||||
|
return &testStore{values: make(map[string][]byte)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore) Load(key string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore) Save(key string, v []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
80
swarm/pss/client/doc.go
Normal file
80
swarm/pss/client/doc.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// simple abstraction for implementing pss functionality
|
||||||
|
//
|
||||||
|
// the pss client library aims to simplify usage of the p2p.protocols package over pss
|
||||||
|
//
|
||||||
|
// IO is performed using the ordinary p2p.MsgReadWriter interface, which transparently communicates with a pss node via RPC using websockets as transport layer, using methods in the PssAPI class in the swarm/pss package
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Minimal-ish usage example (requires a running pss node with websocket RPC):
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "context"
|
||||||
|
// "fmt"
|
||||||
|
// "os"
|
||||||
|
// pss "github.com/ethereum/go-ethereum/swarm/pss/client"
|
||||||
|
// "github.com/ethereum/go-ethereum/p2p/protocols"
|
||||||
|
// "github.com/ethereum/go-ethereum/p2p"
|
||||||
|
// "github.com/ethereum/go-ethereum/pot"
|
||||||
|
// "github.com/ethereum/go-ethereum/log"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// type FooMsg struct {
|
||||||
|
// Bar int
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// func fooHandler (msg interface{}) error {
|
||||||
|
// foomsg, ok := msg.(*FooMsg)
|
||||||
|
// if ok {
|
||||||
|
// log.Debug("Yay, just got a message", "msg", foomsg)
|
||||||
|
// }
|
||||||
|
// return errors.New(fmt.Sprintf("Unknown message"))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// spec := &protocols.Spec{
|
||||||
|
// Name: "foo",
|
||||||
|
// Version: 1,
|
||||||
|
// MaxMsgSize: 1024,
|
||||||
|
// Messages: []interface{}{
|
||||||
|
// FooMsg{},
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// proto := &p2p.Protocol{
|
||||||
|
// Name: spec.Name,
|
||||||
|
// Version: spec.Version,
|
||||||
|
// Length: uint64(len(spec.Messages)),
|
||||||
|
// Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||||
|
// pp := protocols.NewPeer(p, rw, spec)
|
||||||
|
// return pp.Run(fooHandler)
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func implementation() {
|
||||||
|
// cfg := pss.NewClientConfig()
|
||||||
|
// psc := pss.NewClient(context.Background(), nil, cfg)
|
||||||
|
// err := psc.Start()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Crit("can't start pss client")
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.Debug("connected to pss node", "bzz addr", psc.BaseAddr)
|
||||||
|
//
|
||||||
|
// err = psc.RunProtocol(proto)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Crit("can't start protocol on pss websocket")
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// addr := pot.RandomAddress() // should be a real address, of course
|
||||||
|
// psc.AddPssPeer(addr, spec)
|
||||||
|
//
|
||||||
|
// // use the protocol for something
|
||||||
|
//
|
||||||
|
// psc.Stop()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// BUG(test): TestIncoming test times out due to deadlock issues in the swarm hive
|
||||||
|
package client
|
||||||
45
swarm/pss/doc.go
Normal file
45
swarm/pss/doc.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Pss provides devp2p functionality for swarm nodes without the need for a direct tcp connection between them.
|
||||||
|
//
|
||||||
|
// Messages are encapsulated in a devp2p message structure `PssMsg`. These capsules are forwarded from node to node using ordinary tcp devp2p until it reaches its destination: The node or nodes who can successfully decrypt the message.
|
||||||
|
//
|
||||||
|
// Routing of messages is done using swarm's own kademlia routing. Optionally routing can be turned off, forcing the message to be sent to all peers, similar to the behavior of the whisper protocol.
|
||||||
|
//
|
||||||
|
// Pss is intended for messages of limited size, typically a couple of Kbytes at most. The messages themselves can be anything at all; complex data structures or non-descript byte sequences.
|
||||||
|
//
|
||||||
|
// Documentation can be found in the README file.
|
||||||
|
//
|
||||||
|
// For the current state and roadmap of pss development please see https://github.com/ethersphere/swarm/wiki/swarm-dev-progress.
|
||||||
|
//
|
||||||
|
// Please report issues on https://github.com/ethersphere/go-ethereum
|
||||||
|
//
|
||||||
|
// Feel free to ask questions in https://gitter.im/ethersphere/pss
|
||||||
|
//
|
||||||
|
// TOPICS
|
||||||
|
//
|
||||||
|
// An encrypted envelope of a pss messages always contains a Topic. This is pss' way of determining what action to take on the message. The topic is only visible for the node(s) who can decrypt the message.
|
||||||
|
//
|
||||||
|
// This "topic" is not like the subject of an email message, but a hash-like arbitrary 4 byte value. A valid topic can be generated using the `pss_*ToTopic` API methods.
|
||||||
|
//
|
||||||
|
// IDENTITY IN PSS
|
||||||
|
//
|
||||||
|
// Pss aims to achieve perfect darkness. That means that the minimum requirement for two nodes to communicate using pss is a shared secret. This secret can be an arbitrary byte slice, or a ECDSA keypair.
|
||||||
|
//
|
||||||
|
// Peer keys can manually be added to the pss node through its API calls `pss_setPeerPublicKey` and `pss_setSymmetricKey`. Keys are always coupled with a topic, and the keys will only be valid for these topics.
|
||||||
|
//
|
||||||
|
// CONNECTIONS
|
||||||
|
//
|
||||||
|
// A "connection" in pss is a purely virtual construct. There is no mechanisms in place to ensure that the remote peer actually is there. In fact, "adding" a peer involves merely the node's opinion that the peer is there. It may issue messages to that remote peer to a directly connected peer, which in turn passes it on. But if it is not present on the network - or if there is no route to it - the message will never reach its destination through mere forwarding.
|
||||||
|
//
|
||||||
|
// When implementing the devp2p protocol stack, the "adding" of a remote peer is a prerequisite for the side actually initiating the protocol communication. Adding a peer in effect "runs" the protocol on that peer, and adds an internal mapping between a topic and that peer. It also enables sending and receiving messages using the main io-construct in devp2p - the p2p.MsgReadWriter.
|
||||||
|
//
|
||||||
|
// Under the hood, pss implements its own MsgReadWriter, which bridges MsgReadWriter.WriteMsg with Pss.SendRaw, and deftly adds an InjectMsg method which pipes incoming messages to appear on the MsgReadWriter.ReadMsg channel.
|
||||||
|
//
|
||||||
|
// An incoming connection is nothing more than an actual PssMsg appearing with a certain Topic. If a Handler har been registered to that Topic, the message will be passed to it. This constitutes a "new" connection if:
|
||||||
|
//
|
||||||
|
// - The pss node never called AddPeer with this combination of remote peer address and topic, and
|
||||||
|
//
|
||||||
|
// - The pss node never received a PssMsg from this remote peer with this specific Topic before.
|
||||||
|
//
|
||||||
|
// If it is a "new" connection, the protocol will be "run" on the remote peer, in the same manner as if it was pre-emptively added.
|
||||||
|
//
|
||||||
|
package pss
|
||||||
552
swarm/pss/handshake.go
Normal file
552
swarm/pss/handshake.go
Normal file
|
|
@ -0,0 +1,552 @@
|
||||||
|
// +build !nopsshandshake
|
||||||
|
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IsActiveHandshake = true
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctrlSingleton *HandshakeController
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultSymKeyRequestTimeout = 1000 * 8 // max wait ms to receive a response to a handshake symkey request
|
||||||
|
defaultSymKeyExpiryTimeout = 1000 * 10 // ms to wait before allowing garbage collection of an expired symkey
|
||||||
|
defaultSymKeySendLimit = 256 // amount of messages a symkey is valid for
|
||||||
|
defaultSymKeyCapacity = 4 // max number of symkeys to store/send simultaneously
|
||||||
|
)
|
||||||
|
|
||||||
|
// symmetric key exchange message payload
|
||||||
|
type handshakeMsg struct {
|
||||||
|
From []byte
|
||||||
|
Limit uint16
|
||||||
|
Keys [][]byte
|
||||||
|
Request uint8
|
||||||
|
Topic Topic
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal representation of an individual symmetric key
|
||||||
|
type handshakeKey struct {
|
||||||
|
symKeyId *string
|
||||||
|
pubKeyId *string
|
||||||
|
limit uint16
|
||||||
|
count uint16
|
||||||
|
expiredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// container for all in- and outgoing keys
|
||||||
|
// for one particular peer (public key) and topic
|
||||||
|
type handshake struct {
|
||||||
|
outKeys []handshakeKey
|
||||||
|
inKeys []handshakeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization parameters for the HandshakeController
|
||||||
|
//
|
||||||
|
// SymKeyRequestExpiry: Timeout for waiting for a handshake reply
|
||||||
|
// (default 8000 ms)
|
||||||
|
//
|
||||||
|
// SymKeySendLimit: Amount of messages symmetric keys issues by
|
||||||
|
// this node is valid for (default 256)
|
||||||
|
//
|
||||||
|
// SymKeyCapacity: Ideal (and maximum) amount of symmetric keys
|
||||||
|
// held per direction per peer (default 4)
|
||||||
|
type HandshakeParams struct {
|
||||||
|
SymKeyRequestTimeout time.Duration
|
||||||
|
SymKeyExpiryTimeout time.Duration
|
||||||
|
SymKeySendLimit uint16
|
||||||
|
SymKeyCapacity uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sane defaults for HandshakeController initialization
|
||||||
|
func NewHandshakeParams() *HandshakeParams {
|
||||||
|
return &HandshakeParams{
|
||||||
|
SymKeyRequestTimeout: defaultSymKeyRequestTimeout * time.Millisecond,
|
||||||
|
SymKeyExpiryTimeout: defaultSymKeyExpiryTimeout * time.Millisecond,
|
||||||
|
SymKeySendLimit: defaultSymKeySendLimit,
|
||||||
|
SymKeyCapacity: defaultSymKeyCapacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton object enabling semi-automatic Diffie-Hellman
|
||||||
|
// exchange of ephemeral symmetric keys
|
||||||
|
type HandshakeController struct {
|
||||||
|
pss *Pss
|
||||||
|
keyC map[string]chan []string // adds a channel to report when a handshake succeeds
|
||||||
|
lock sync.Mutex
|
||||||
|
symKeyRequestTimeout time.Duration
|
||||||
|
symKeyExpiryTimeout time.Duration
|
||||||
|
symKeySendLimit uint16
|
||||||
|
symKeyCapacity uint8
|
||||||
|
symKeyIndex map[string]*handshakeKey
|
||||||
|
handshakes map[string]map[Topic]*handshake
|
||||||
|
deregisterFuncs map[Topic]func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach HandshakeController to pss node
|
||||||
|
//
|
||||||
|
// Must be called before starting the pss node service
|
||||||
|
func SetHandshakeController(pss *Pss, params *HandshakeParams) error {
|
||||||
|
ctrl := &HandshakeController{
|
||||||
|
pss: pss,
|
||||||
|
keyC: make(map[string]chan []string),
|
||||||
|
symKeyRequestTimeout: params.SymKeyRequestTimeout,
|
||||||
|
symKeyExpiryTimeout: params.SymKeyExpiryTimeout,
|
||||||
|
symKeySendLimit: params.SymKeySendLimit,
|
||||||
|
symKeyCapacity: params.SymKeyCapacity,
|
||||||
|
symKeyIndex: make(map[string]*handshakeKey),
|
||||||
|
handshakes: make(map[string]map[Topic]*handshake),
|
||||||
|
deregisterFuncs: make(map[Topic]func()),
|
||||||
|
}
|
||||||
|
api := &HandshakeAPI{
|
||||||
|
namespace: "pss",
|
||||||
|
ctrl: ctrl,
|
||||||
|
}
|
||||||
|
pss.addAPI(rpc.API{
|
||||||
|
Namespace: api.namespace,
|
||||||
|
Version: "0.2",
|
||||||
|
Service: api,
|
||||||
|
Public: true,
|
||||||
|
})
|
||||||
|
ctrlSingleton = ctrl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all unexpired symmetric keys from store by
|
||||||
|
// peer (public key), topic and specified direction
|
||||||
|
func (self *HandshakeController) validKeys(pubkeyid string, topic *Topic, in bool) (validkeys []*string) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
now := time.Now()
|
||||||
|
if _, ok := self.handshakes[pubkeyid]; !ok {
|
||||||
|
return []*string{}
|
||||||
|
} else if _, ok := self.handshakes[pubkeyid][*topic]; !ok {
|
||||||
|
return []*string{}
|
||||||
|
}
|
||||||
|
var keystore *[]handshakeKey
|
||||||
|
if in {
|
||||||
|
keystore = &(self.handshakes[pubkeyid][*topic].inKeys)
|
||||||
|
} else {
|
||||||
|
keystore = &(self.handshakes[pubkeyid][*topic].outKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range *keystore {
|
||||||
|
if key.limit <= key.count {
|
||||||
|
self.releaseKey(*key.symKeyId, topic)
|
||||||
|
} else if !key.expiredAt.IsZero() && key.expiredAt.Before(now) {
|
||||||
|
self.releaseKey(*key.symKeyId, topic)
|
||||||
|
} else {
|
||||||
|
validkeys = append(validkeys, key.symKeyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all given symmetric keys with validity limits to store by
|
||||||
|
// peer (public key), topic and specified direction
|
||||||
|
func (self *HandshakeController) updateKeys(pubkeyid string, topic *Topic, in bool, symkeyids []string, limit uint16) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
if _, ok := self.handshakes[pubkeyid]; !ok {
|
||||||
|
self.handshakes[pubkeyid] = make(map[Topic]*handshake)
|
||||||
|
|
||||||
|
}
|
||||||
|
if self.handshakes[pubkeyid][*topic] == nil {
|
||||||
|
self.handshakes[pubkeyid][*topic] = &handshake{}
|
||||||
|
}
|
||||||
|
var keystore *[]handshakeKey
|
||||||
|
expire := time.Now()
|
||||||
|
if in {
|
||||||
|
keystore = &(self.handshakes[pubkeyid][*topic].inKeys)
|
||||||
|
} else {
|
||||||
|
keystore = &(self.handshakes[pubkeyid][*topic].outKeys)
|
||||||
|
expire = expire.Add(time.Millisecond * self.symKeyExpiryTimeout)
|
||||||
|
}
|
||||||
|
for _, storekey := range *keystore {
|
||||||
|
storekey.expiredAt = expire
|
||||||
|
}
|
||||||
|
for i := 0; i < len(symkeyids); i++ {
|
||||||
|
storekey := handshakeKey{
|
||||||
|
symKeyId: &symkeyids[i],
|
||||||
|
pubKeyId: &pubkeyid,
|
||||||
|
limit: limit,
|
||||||
|
}
|
||||||
|
*keystore = append(*keystore, storekey)
|
||||||
|
self.pss.symKeyPool[*storekey.symKeyId][*topic].protected = true
|
||||||
|
}
|
||||||
|
for i := 0; i < len(*keystore); i++ {
|
||||||
|
self.symKeyIndex[*(*keystore)[i].symKeyId] = &((*keystore)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire a symmetric key, making it elegible for garbage collection
|
||||||
|
func (self *HandshakeController) releaseKey(symkeyid string, topic *Topic) bool {
|
||||||
|
if self.symKeyIndex[symkeyid] == nil {
|
||||||
|
log.Debug("no symkey", "symkeyid", symkeyid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
self.symKeyIndex[symkeyid].expiredAt = time.Now()
|
||||||
|
log.Debug("handshake release", "symkeyid", symkeyid)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks all symmetric keys in given direction(s) by
|
||||||
|
// specified peer (public key) and topic for expiry.
|
||||||
|
// Expired means:
|
||||||
|
// - expiry timestamp is set, and grace period is exceeded
|
||||||
|
// - message validity limit is reached
|
||||||
|
func (self *HandshakeController) cleanHandshake(pubkeyid string, topic *Topic, in bool, out bool) int {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
var deletecount int
|
||||||
|
var deletes []string
|
||||||
|
now := time.Now()
|
||||||
|
handshake := self.handshakes[pubkeyid][*topic]
|
||||||
|
log.Debug("handshake clean", "pubkey", pubkeyid, "topic", topic)
|
||||||
|
if in {
|
||||||
|
for i, key := range handshake.inKeys {
|
||||||
|
if key.expiredAt.Before(now) || (key.expiredAt.IsZero() && key.limit <= key.count) {
|
||||||
|
log.Trace("handshake in clean remove", "symkeyid", *key.symKeyId)
|
||||||
|
deletes = append(deletes, *key.symKeyId)
|
||||||
|
handshake.inKeys[deletecount] = handshake.inKeys[i]
|
||||||
|
deletecount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handshake.inKeys = handshake.inKeys[:len(handshake.inKeys)-deletecount]
|
||||||
|
}
|
||||||
|
if out {
|
||||||
|
deletecount = 0
|
||||||
|
for i, key := range handshake.outKeys {
|
||||||
|
if key.expiredAt.Before(now) && (key.expiredAt.IsZero() && key.limit <= key.count) {
|
||||||
|
log.Trace("handshake out clean remove", "symkeyid", *key.symKeyId)
|
||||||
|
deletes = append(deletes, *key.symKeyId)
|
||||||
|
handshake.outKeys[deletecount] = handshake.outKeys[i]
|
||||||
|
deletecount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handshake.outKeys = handshake.outKeys[:len(handshake.outKeys)-deletecount]
|
||||||
|
}
|
||||||
|
for _, keyid := range deletes {
|
||||||
|
delete(self.symKeyIndex, keyid)
|
||||||
|
self.pss.symKeyPool[keyid][*topic].protected = false
|
||||||
|
}
|
||||||
|
return len(deletes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs cleanHandshake() on all peers and topics
|
||||||
|
func (self *HandshakeController) clean() {
|
||||||
|
peerpubkeys := self.handshakes
|
||||||
|
for pubkeyid, peertopics := range peerpubkeys {
|
||||||
|
for topic, _ := range peertopics {
|
||||||
|
self.cleanHandshake(pubkeyid, &topic, true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed as a PssMsg handler for the topic handshake is activated on
|
||||||
|
// Handles incoming key exchange messages and
|
||||||
|
// ccunts message usage by symmetric key (expiry limit control)
|
||||||
|
// Only returns error if key handler fails
|
||||||
|
func (self *HandshakeController) handler(msg []byte, p *p2p.Peer, asymmetric bool, symkeyid string) error {
|
||||||
|
if !asymmetric {
|
||||||
|
if self.symKeyIndex[symkeyid] != nil {
|
||||||
|
if self.symKeyIndex[symkeyid].count >= self.symKeyIndex[symkeyid].limit {
|
||||||
|
return fmt.Errorf("discarding message using expired key", "symkeyid", symkeyid)
|
||||||
|
}
|
||||||
|
self.symKeyIndex[symkeyid].count++
|
||||||
|
log.Trace("increment symkey recv use", "symsymkeyid", symkeyid, "count", self.symKeyIndex[symkeyid].count, "limit", self.symKeyIndex[symkeyid].limit, "receiver", common.ToHex(crypto.FromECDSAPub(self.pss.PublicKey())))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
keymsg := &handshakeMsg{}
|
||||||
|
err := rlp.DecodeBytes(msg, keymsg)
|
||||||
|
if err == nil {
|
||||||
|
err := self.handleKeys(symkeyid, keymsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("handlekeys fail", "error", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming key exchange message
|
||||||
|
// Add keys received from peer to store
|
||||||
|
// and enerate and send the amount of keys requested by peer
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// - flood guard
|
||||||
|
// - keylength check
|
||||||
|
// - update address hint if:
|
||||||
|
// 1) leftmost bytes in new address do not match stored
|
||||||
|
// 2) else, if new address is longer
|
||||||
|
func (self *HandshakeController) handleKeys(pubkeyid string, keymsg *handshakeMsg) error {
|
||||||
|
// new keys from peer
|
||||||
|
if len(keymsg.Keys) > 0 {
|
||||||
|
log.Debug("received handshake keys", "pubkeyid", pubkeyid, "from", keymsg.From, "count", len(keymsg.Keys))
|
||||||
|
var sendsymkeyids []string
|
||||||
|
for _, key := range keymsg.Keys {
|
||||||
|
sendsymkey := make([]byte, len(key))
|
||||||
|
copy(sendsymkey, key)
|
||||||
|
var address PssAddress
|
||||||
|
copy(address[:], keymsg.From)
|
||||||
|
sendsymkeyid, err := self.pss.SetSymmetricKey(sendsymkey, keymsg.Topic, &address, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sendsymkeyids = append(sendsymkeyids, sendsymkeyid)
|
||||||
|
}
|
||||||
|
if len(sendsymkeyids) > 0 {
|
||||||
|
self.updateKeys(pubkeyid, &keymsg.Topic, false, sendsymkeyids, keymsg.Limit)
|
||||||
|
|
||||||
|
self.alertHandshake(pubkeyid, sendsymkeyids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// peer request for keys
|
||||||
|
if keymsg.Request > 0 {
|
||||||
|
_, err := self.sendKey(pubkeyid, &keymsg.Topic, keymsg.Request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send key exchange to peer (public key) valid for `topic`
|
||||||
|
// Will send number of keys specified by `keycount` with
|
||||||
|
// validity limits specified in `msglimit`
|
||||||
|
// If number of valid outgoing keys is less than the ideal/max
|
||||||
|
// amount, a request is sent for the amount of keys to make up
|
||||||
|
// the difference
|
||||||
|
func (self *HandshakeController) sendKey(pubkeyid string, topic *Topic, keycount uint8) ([]string, error) {
|
||||||
|
|
||||||
|
var requestcount uint8
|
||||||
|
to := &PssAddress{}
|
||||||
|
if _, ok := self.pss.pubKeyPool[pubkeyid]; !ok {
|
||||||
|
return []string{}, errors.New("Invalid public key")
|
||||||
|
} else if psp, ok := self.pss.pubKeyPool[pubkeyid][*topic]; ok {
|
||||||
|
to = psp.address
|
||||||
|
}
|
||||||
|
|
||||||
|
recvkeys := make([][]byte, keycount)
|
||||||
|
recvkeyids := make([]string, keycount)
|
||||||
|
self.lock.Lock()
|
||||||
|
if _, ok := self.handshakes[pubkeyid]; !ok {
|
||||||
|
self.handshakes[pubkeyid] = make(map[Topic]*handshake)
|
||||||
|
}
|
||||||
|
self.lock.Unlock()
|
||||||
|
|
||||||
|
// check if buffer is not full
|
||||||
|
outkeys := self.validKeys(pubkeyid, topic, false)
|
||||||
|
if len(outkeys) < int(self.symKeyCapacity) {
|
||||||
|
//requestcount = uint8(self.symKeyCapacity - uint8(len(outkeys)))
|
||||||
|
requestcount = self.symKeyCapacity
|
||||||
|
}
|
||||||
|
// return if there's nothing to be accomplished
|
||||||
|
if requestcount == 0 && keycount == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new keys to send
|
||||||
|
for i := 0; i < len(recvkeyids); i++ {
|
||||||
|
var err error
|
||||||
|
recvkeyids[i], err = self.pss.generateSymmetricKey(*topic, to, true)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, fmt.Errorf("set receive symkey fail (pubkey %x topic %x): %v", pubkeyid, topic, err)
|
||||||
|
}
|
||||||
|
recvkeys[i], err = self.pss.GetSymmetricKey(recvkeyids[i])
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, fmt.Errorf("GET Generated outgoing symkey fail (pubkey %x topic %x): %v", pubkeyid, topic, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.updateKeys(pubkeyid, topic, true, recvkeyids, self.symKeySendLimit)
|
||||||
|
|
||||||
|
// encode and send the message
|
||||||
|
recvkeymsg := &handshakeMsg{
|
||||||
|
From: self.pss.BaseAddr(),
|
||||||
|
Keys: recvkeys,
|
||||||
|
Request: requestcount,
|
||||||
|
Limit: self.symKeySendLimit,
|
||||||
|
Topic: *topic,
|
||||||
|
}
|
||||||
|
log.Debug("sending our symkeys", "pubkey", pubkeyid, "symkeys", recvkeyids, "limit", self.symKeySendLimit, "requestcount", requestcount, "keycount", len(recvkeys))
|
||||||
|
recvkeybytes, err := rlp.EncodeToBytes(recvkeymsg)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, fmt.Errorf("rlp keymsg encode fail: %v", err)
|
||||||
|
}
|
||||||
|
// if the send fails it means this public key is not registered for this particular address AND topic
|
||||||
|
err = self.pss.SendAsym(pubkeyid, *topic, recvkeybytes)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, fmt.Errorf("Send symkey failed: %v", err)
|
||||||
|
}
|
||||||
|
return recvkeyids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enables callback for keys received from a key exchange request
|
||||||
|
func (self *HandshakeController) alertHandshake(pubkeyid string, symkeys []string) chan []string {
|
||||||
|
if len(symkeys) > 0 {
|
||||||
|
if _, ok := self.keyC[pubkeyid]; ok {
|
||||||
|
self.keyC[pubkeyid] <- symkeys
|
||||||
|
close(self.keyC[pubkeyid])
|
||||||
|
delete(self.keyC, pubkeyid)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
if _, ok := self.keyC[pubkeyid]; !ok {
|
||||||
|
self.keyC[pubkeyid] = make(chan []string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.keyC[pubkeyid]
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandshakeAPI struct {
|
||||||
|
namespace string
|
||||||
|
ctrl *HandshakeController
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate a handshake session for a peer (public key) and topic
|
||||||
|
// combination.
|
||||||
|
//
|
||||||
|
// If `sync` is set, the call will block until keys are received from peer,
|
||||||
|
// or if the handshake request times out
|
||||||
|
//
|
||||||
|
// If `flush` is set, the max amount of keys will be sent to the peer
|
||||||
|
// regardless of how many valid keys that currently exist in the store.
|
||||||
|
//
|
||||||
|
// Returns list of symmetric key ids that can be passed to pss.GetSymmetricKey()
|
||||||
|
// for retrieval of the symmetric key bytes themselves.
|
||||||
|
//
|
||||||
|
// Fails if the incoming symmetric key store is already full (and `flush` is false),
|
||||||
|
// or if the underlying key dispatcher fails
|
||||||
|
func (self *HandshakeAPI) Handshake(pubkeyid string, topic Topic, sync bool, flush bool) (keys []string, err error) {
|
||||||
|
var hsc chan []string
|
||||||
|
var keycount uint8
|
||||||
|
if flush {
|
||||||
|
keycount = self.ctrl.symKeyCapacity
|
||||||
|
} else {
|
||||||
|
validkeys := self.ctrl.validKeys(pubkeyid, &topic, false)
|
||||||
|
keycount = uint8(self.ctrl.symKeyCapacity - uint8(len(validkeys)))
|
||||||
|
}
|
||||||
|
if keycount == 0 {
|
||||||
|
return keys, errors.New("Incoming symmetric key store is already full")
|
||||||
|
}
|
||||||
|
if sync {
|
||||||
|
hsc = self.ctrl.alertHandshake(pubkeyid, []string{})
|
||||||
|
}
|
||||||
|
_, err = self.ctrl.sendKey(pubkeyid, &topic, keycount)
|
||||||
|
if err != nil {
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
if sync {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), self.ctrl.symKeyRequestTimeout)
|
||||||
|
select {
|
||||||
|
case keys = <-hsc:
|
||||||
|
log.Trace("sync handshake response receive", "key", keys)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return []string{}, errors.New("timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate handshake functionality on a topic
|
||||||
|
func (self *HandshakeAPI) AddHandshake(topic Topic) error {
|
||||||
|
self.ctrl.deregisterFuncs[topic] = self.ctrl.pss.Register(&topic, self.ctrl.handler)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate handshake functionalty on a topic
|
||||||
|
func (self *HandshakeAPI) RemoveHandshake(topic *Topic) error {
|
||||||
|
if _, ok := self.ctrl.deregisterFuncs[*topic]; ok {
|
||||||
|
self.ctrl.deregisterFuncs[*topic]()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all valid symmetric keys in store per peer (public key)
|
||||||
|
// and topic.
|
||||||
|
//
|
||||||
|
// The `in` and `out` parameters indicate for which direction(s)
|
||||||
|
// symmetric keys will be returned.
|
||||||
|
// If both are false, no keys (and no error) will be returned.
|
||||||
|
func (self *HandshakeAPI) GetHandshakeKeys(pubkeyid string, topic Topic, in bool, out bool) (keys []string, err error) {
|
||||||
|
if in {
|
||||||
|
for _, inkey := range self.ctrl.validKeys(pubkeyid, &topic, true) {
|
||||||
|
keys = append(keys, *inkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out {
|
||||||
|
for _, outkey := range self.ctrl.validKeys(pubkeyid, &topic, false) {
|
||||||
|
keys = append(keys, *outkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the amount of messages the specified symmetric key
|
||||||
|
// is still valid for under the handshake scheme
|
||||||
|
func (self *HandshakeAPI) GetHandshakeKeyCapacity(symkeyid string) (uint16, error) {
|
||||||
|
storekey := self.ctrl.symKeyIndex[symkeyid]
|
||||||
|
if storekey == nil {
|
||||||
|
return 0, fmt.Errorf("invalid symkey id %s", symkeyid)
|
||||||
|
}
|
||||||
|
return storekey.limit - storekey.count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the byte representation of the public key in ascii hex
|
||||||
|
// associated with the given symmetric key
|
||||||
|
func (self *HandshakeAPI) GetHandshakePublicKey(symkeyid string) (string, error) {
|
||||||
|
storekey := self.ctrl.symKeyIndex[symkeyid]
|
||||||
|
if storekey == nil {
|
||||||
|
return "", fmt.Errorf("invalid symkey id %s", symkeyid)
|
||||||
|
}
|
||||||
|
return *storekey.pubKeyId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually expire the given symkey
|
||||||
|
//
|
||||||
|
// If `flush` is set, garbage collection will be performed before returning.
|
||||||
|
//
|
||||||
|
// Returns true on successful removal, false otherwise
|
||||||
|
func (self *HandshakeAPI) ReleaseHandshakeKey(pubkeyid string, topic Topic, symkeyid string, flush bool) (removed bool, err error) {
|
||||||
|
removed = self.ctrl.releaseKey(symkeyid, &topic)
|
||||||
|
if removed && flush {
|
||||||
|
self.ctrl.cleanHandshake(pubkeyid, &topic, true, true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send symmetric message under the handshake scheme
|
||||||
|
//
|
||||||
|
// Overloads the pss.SendSym() API call, adding symmetric key usage count
|
||||||
|
// for message expiry control
|
||||||
|
func (self *HandshakeAPI) SendSym(symkeyid string, topic Topic, msg hexutil.Bytes) (err error) {
|
||||||
|
err = self.ctrl.pss.SendSym(symkeyid, topic, msg[:])
|
||||||
|
if self.ctrl.symKeyIndex[symkeyid] != nil {
|
||||||
|
if self.ctrl.symKeyIndex[symkeyid].count >= self.ctrl.symKeyIndex[symkeyid].limit {
|
||||||
|
return errors.New("attempted send with expired key")
|
||||||
|
}
|
||||||
|
self.ctrl.symKeyIndex[symkeyid].count++
|
||||||
|
log.Trace("increment symkey send use", "symkeyid", symkeyid, "count", self.ctrl.symKeyIndex[symkeyid].count, "limit", self.ctrl.symKeyIndex[symkeyid].limit, "receiver", common.ToHex(crypto.FromECDSAPub(self.ctrl.pss.PublicKey())))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
11
swarm/pss/handshake_none.go
Normal file
11
swarm/pss/handshake_none.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build nopsshandshake
|
||||||
|
|
||||||
|
package pss
|
||||||
|
|
||||||
|
const (
|
||||||
|
IsActiveHandshake = false
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHandshakeParams() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
248
swarm/pss/handshake_test.go
Normal file
248
swarm/pss/handshake_test.go
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// asymmetrical key exchange between two directly connected peers
|
||||||
|
// full address, partial address (8 bytes) and empty address
|
||||||
|
func TestHandshake(t *testing.T) {
|
||||||
|
t.Run("32", testHandshake)
|
||||||
|
t.Run("8", testHandshake)
|
||||||
|
t.Run("0", testHandshake)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandshake(t *testing.T) {
|
||||||
|
|
||||||
|
// how much of the address we will use
|
||||||
|
useHandshake = true
|
||||||
|
var addrsize int64
|
||||||
|
var err error
|
||||||
|
addrsizestring := strings.Split(t.Name(), "/")
|
||||||
|
addrsize, _ = strconv.ParseInt(addrsizestring[1], 10, 0)
|
||||||
|
|
||||||
|
// set up two nodes directly connected
|
||||||
|
// (we are not testing pss routing here)
|
||||||
|
clients, err := setupNetwork(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var topic string
|
||||||
|
err = clients[0].Call(&topic, "pss_stringToTopic", "foo:42")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loaddr string
|
||||||
|
err = clients[0].Call(&loaddr, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
// "0x" = 2 bytes + addrsize address bytes which in hex is 2x length
|
||||||
|
loaddr = loaddr[:2+(addrsize*2)]
|
||||||
|
var roaddr string
|
||||||
|
err = clients[1].Call(&roaddr, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
roaddr = roaddr[:2+(addrsize*2)]
|
||||||
|
log.Debug("addresses", "left", loaddr, "right", roaddr)
|
||||||
|
|
||||||
|
// retrieve public key from pss instance
|
||||||
|
// set this public key reciprocally
|
||||||
|
var lpubkey string
|
||||||
|
err = clients[0].Call(&lpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
var rpubkey string
|
||||||
|
err = clients[1].Call(&rpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 1000) // replace with hive healthy code
|
||||||
|
|
||||||
|
// give each node its peer's public key
|
||||||
|
err = clients[0].Call(nil, "pss_setPeerPublicKey", rpubkey, topic, roaddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = clients[1].Call(nil, "pss_setPeerPublicKey", lpubkey, topic, loaddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform the handshake
|
||||||
|
// after this each side will have defaultSymKeyBufferCapacity symkeys each for in- and outgoing messages:
|
||||||
|
// L -> request 4 keys -> R
|
||||||
|
// L <- send 4 keys, request 4 keys <- R
|
||||||
|
// L -> send 4 keys -> R
|
||||||
|
// the call will fill the array with symkeys L needs for sending to R
|
||||||
|
err = clients[0].Call(nil, "pss_addHandshake", topic)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = clients[1].Call(nil, "pss_addHandshake", topic)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lhsendsymkeyids []string
|
||||||
|
err = clients[0].Call(&lhsendsymkeyids, "pss_handshake", rpubkey, topic, true, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the r-node gets its keys
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
// check if we have 6 outgoing keys stored, and they match what was received from R
|
||||||
|
var lsendsymkeyids []string
|
||||||
|
err = clients[0].Call(&lsendsymkeyids, "pss_getHandshakeKeys", rpubkey, topic, false, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := 0
|
||||||
|
for _, hid := range lhsendsymkeyids {
|
||||||
|
for _, lid := range lsendsymkeyids {
|
||||||
|
if lid == hid {
|
||||||
|
m++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m != defaultSymKeyCapacity {
|
||||||
|
t.Fatalf("buffer size mismatch, expected %d, have %d: %v", defaultSymKeyCapacity, m, lsendsymkeyids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if in- and outgoing keys on l-node and r-node match up and are in opposite categories (l recv = r send, l send = r recv)
|
||||||
|
var rsendsymkeyids []string
|
||||||
|
err = clients[1].Call(&rsendsymkeyids, "pss_getHandshakeKeys", lpubkey, topic, false, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var lrecvsymkeyids []string
|
||||||
|
err = clients[0].Call(&lrecvsymkeyids, "pss_getHandshakeKeys", rpubkey, topic, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var rrecvsymkeyids []string
|
||||||
|
err = clients[1].Call(&rrecvsymkeyids, "pss_getHandshakeKeys", lpubkey, topic, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get outgoing symkeys in byte form from both sides
|
||||||
|
var lsendsymkeys []string
|
||||||
|
for _, id := range lsendsymkeyids {
|
||||||
|
var key string
|
||||||
|
err = clients[0].Call(&key, "pss_getSymmetricKey", id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lsendsymkeys = append(lsendsymkeys, key)
|
||||||
|
}
|
||||||
|
var rsendsymkeys []string
|
||||||
|
for _, id := range rsendsymkeyids {
|
||||||
|
var key string
|
||||||
|
err = clients[1].Call(&key, "pss_getSymmetricKey", id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rsendsymkeys = append(rsendsymkeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get incoming symkeys in byte form from both sides and compare
|
||||||
|
var lrecvsymkeys []string
|
||||||
|
for _, id := range lrecvsymkeyids {
|
||||||
|
var key string
|
||||||
|
err = clients[0].Call(&key, "pss_getSymmetricKey", id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
match := false
|
||||||
|
for _, otherkey := range rsendsymkeys {
|
||||||
|
if otherkey == key {
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("no match right send for left recv key %s", id)
|
||||||
|
}
|
||||||
|
lrecvsymkeys = append(lrecvsymkeys, key)
|
||||||
|
}
|
||||||
|
var rrecvsymkeys []string
|
||||||
|
for _, id := range rrecvsymkeyids {
|
||||||
|
var key string
|
||||||
|
err = clients[1].Call(&key, "pss_getSymmetricKey", id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
match := false
|
||||||
|
for _, otherkey := range lsendsymkeys {
|
||||||
|
if otherkey == key {
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Fatalf("no match left send for right recv key %s", id)
|
||||||
|
}
|
||||||
|
rrecvsymkeys = append(rrecvsymkeys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send new handshake request, should send no keys
|
||||||
|
err = clients[0].Call(nil, "pss_handshake", rpubkey, topic, false)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected full symkey buffer error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// expire one key, send new handshake request
|
||||||
|
err = clients[0].Call(nil, "pss_releaseHandshakeKey", rpubkey, topic, lsendsymkeyids[0], true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("release left send key %s fail: %v", lsendsymkeyids[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newlhsendkeyids []string
|
||||||
|
|
||||||
|
// send new handshake request, should now receive one key
|
||||||
|
// check that it is not in previous right recv key array
|
||||||
|
err = clients[0].Call(&newlhsendkeyids, "pss_handshake", rpubkey, topic, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("handshake send fail: %v", err)
|
||||||
|
} else if len(newlhsendkeyids) != defaultSymKeyCapacity {
|
||||||
|
t.Fatalf("wrong receive count, expected 1, got %d", len(newlhsendkeyids))
|
||||||
|
}
|
||||||
|
|
||||||
|
var newlrecvsymkey string
|
||||||
|
err = clients[0].Call(&newlrecvsymkey, "pss_getSymmetricKey", newlhsendkeyids[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var rmatchsymkeyid *string
|
||||||
|
for i, id := range rrecvsymkeyids {
|
||||||
|
var key string
|
||||||
|
err = clients[1].Call(&key, "pss_getSymmetricKey", id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if newlrecvsymkey == key {
|
||||||
|
rmatchsymkeyid = &rrecvsymkeyids[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rmatchsymkeyid != nil {
|
||||||
|
t.Fatalf("right sent old key id %s in second handshake", *rmatchsymkeyid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean the pss core keystore. Should clean the key released earlier
|
||||||
|
var cleancount int
|
||||||
|
clients[0].Call(&cleancount, "psstest_clean")
|
||||||
|
if cleancount > 1 {
|
||||||
|
t.Fatalf("pss clean count mismatch; expected 1, got %d", cleancount)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
swarm/pss/ping.go
Normal file
80
swarm/pss/ping.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// +build !nopssprotocol,!nopssping
|
||||||
|
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/protocols"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generic ping protocol implementation for
|
||||||
|
// pss devp2p protocol emulation
|
||||||
|
type PingMsg struct {
|
||||||
|
Created time.Time
|
||||||
|
Pong bool // set if message is pong reply
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ping struct {
|
||||||
|
Pong bool // toggle pong reply upon ping receive
|
||||||
|
OutC chan bool // trigger ping
|
||||||
|
InC chan bool // optional, report back to calling code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Ping) pingHandler(msg interface{}) error {
|
||||||
|
var pingmsg *PingMsg
|
||||||
|
var ok bool
|
||||||
|
if pingmsg, ok = msg.(*PingMsg); !ok {
|
||||||
|
return errors.New("invalid msg")
|
||||||
|
}
|
||||||
|
log.Debug("ping handler", "msg", pingmsg, "outc", self.OutC)
|
||||||
|
if self.InC != nil {
|
||||||
|
self.InC <- pingmsg.Pong
|
||||||
|
}
|
||||||
|
if self.Pong && !pingmsg.Pong {
|
||||||
|
self.OutC <- true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var PingProtocol = &protocols.Spec{
|
||||||
|
Name: "psstest",
|
||||||
|
Version: 1,
|
||||||
|
MaxMsgSize: 1024,
|
||||||
|
Messages: []interface{}{
|
||||||
|
PingMsg{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var PingTopic = ProtocolTopic(PingProtocol)
|
||||||
|
|
||||||
|
func NewPingProtocol(ping *Ping) *p2p.Protocol {
|
||||||
|
return &p2p.Protocol{
|
||||||
|
Name: PingProtocol.Name,
|
||||||
|
Version: PingProtocol.Version,
|
||||||
|
Length: uint64(PingProtocol.MaxMsgSize),
|
||||||
|
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||||
|
quitC := make(chan struct{})
|
||||||
|
pp := protocols.NewPeer(p, rw, PingProtocol)
|
||||||
|
log.Trace("running pss vprotocol", "peer", p, "outc", ping.OutC)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ispong := <-ping.OutC:
|
||||||
|
pp.Send(&PingMsg{
|
||||||
|
Created: time.Now(),
|
||||||
|
Pong: ispong,
|
||||||
|
})
|
||||||
|
case <-quitC:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := pp.Run(ping.pingHandler)
|
||||||
|
quitC <- struct{}{}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
235
swarm/pss/protocol.go
Normal file
235
swarm/pss/protocol.go
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
// +build !nopssprotocol
|
||||||
|
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/protocols"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IsActiveProtocol = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Convenience wrapper for devp2p protocol messages for transport over pss
|
||||||
|
type ProtocolMsg struct {
|
||||||
|
Code uint64
|
||||||
|
Size uint32
|
||||||
|
Payload []byte
|
||||||
|
ReceivedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a ProtocolMsg
|
||||||
|
func NewProtocolMsg(code uint64, msg interface{}) ([]byte, error) {
|
||||||
|
|
||||||
|
rlpdata, err := rlp.EncodeToBytes(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO verify that nested structs cannot be used in rlp
|
||||||
|
smsg := &ProtocolMsg{
|
||||||
|
Code: code,
|
||||||
|
Size: uint32(len(rlpdata)),
|
||||||
|
Payload: rlpdata,
|
||||||
|
}
|
||||||
|
|
||||||
|
return rlp.EncodeToBytes(smsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol options to be passed to a new Protocol instance
|
||||||
|
//
|
||||||
|
// The parameters specify which encryption schemes to allow
|
||||||
|
type ProtocolParams struct {
|
||||||
|
Asymmetric bool
|
||||||
|
Symmetric bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PssReadWriter bridges pss send/receive with devp2p protocol send/receive
|
||||||
|
//
|
||||||
|
// Implements p2p.MsgReadWriter
|
||||||
|
type PssReadWriter struct {
|
||||||
|
*Pss
|
||||||
|
LastActive time.Time
|
||||||
|
rw chan p2p.Msg
|
||||||
|
spec *protocols.Spec
|
||||||
|
topic *Topic
|
||||||
|
sendFunc func(string, Topic, []byte) error
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements p2p.MsgReader
|
||||||
|
func (prw *PssReadWriter) ReadMsg() (p2p.Msg, error) {
|
||||||
|
msg := <-prw.rw
|
||||||
|
log.Trace(fmt.Sprintf("pssrw readmsg: %v", msg))
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements p2p.MsgWriter
|
||||||
|
func (prw *PssReadWriter) WriteMsg(msg p2p.Msg) error {
|
||||||
|
log.Trace("pssrw writemsg", "msg", msg)
|
||||||
|
rlpdata := make([]byte, msg.Size)
|
||||||
|
msg.Payload.Read(rlpdata)
|
||||||
|
pmsg, err := rlp.EncodeToBytes(ProtocolMsg{
|
||||||
|
Code: msg.Code,
|
||||||
|
Size: msg.Size,
|
||||||
|
Payload: rlpdata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return prw.sendFunc(prw.key, *prw.topic, pmsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Injects a p2p.Msg into the MsgReadWriter, so that it appears on the associated p2p.MsgReader
|
||||||
|
func (prw *PssReadWriter) injectMsg(msg p2p.Msg) error {
|
||||||
|
log.Trace(fmt.Sprintf("pssrw injectmsg: %v", msg))
|
||||||
|
prw.rw <- msg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience object for emulation devp2p over pss
|
||||||
|
type Protocol struct {
|
||||||
|
*Pss
|
||||||
|
proto *p2p.Protocol
|
||||||
|
topic *Topic
|
||||||
|
spec *protocols.Spec
|
||||||
|
pubKeyRWPool map[string]p2p.MsgReadWriter
|
||||||
|
symKeyRWPool map[string]p2p.MsgReadWriter
|
||||||
|
Asymmetric bool
|
||||||
|
Symmetric bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activates devp2p emulation over a specific pss topic
|
||||||
|
//
|
||||||
|
// One or both encryption schemes must be specified. If
|
||||||
|
// only one is specified, the protocol will not be valid
|
||||||
|
// for the other, and will make the message handler
|
||||||
|
// return errors
|
||||||
|
func RegisterProtocol(ps *Pss, topic *Topic, spec *protocols.Spec, targetprotocol *p2p.Protocol, options *ProtocolParams) (*Protocol, error) {
|
||||||
|
if !options.Asymmetric && !options.Symmetric {
|
||||||
|
return nil, fmt.Errorf("specify at least one of asymmetric or symmetric messaging mode")
|
||||||
|
}
|
||||||
|
pp := &Protocol{
|
||||||
|
Pss: ps,
|
||||||
|
proto: targetprotocol,
|
||||||
|
topic: topic,
|
||||||
|
spec: spec,
|
||||||
|
pubKeyRWPool: make(map[string]p2p.MsgReadWriter),
|
||||||
|
symKeyRWPool: make(map[string]p2p.MsgReadWriter),
|
||||||
|
Asymmetric: options.Asymmetric,
|
||||||
|
Symmetric: options.Symmetric,
|
||||||
|
}
|
||||||
|
return pp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic handler for incoming messages over devp2p emulation
|
||||||
|
//
|
||||||
|
// To be passed to pss.Register()
|
||||||
|
//
|
||||||
|
// Will run the protocol on a new incoming peer, provided that
|
||||||
|
// the encryption key of the message has a match in the internal
|
||||||
|
// pss keypool
|
||||||
|
//
|
||||||
|
// Fails if protocol is not valid for the message encryption scheme,
|
||||||
|
// if adding a new peer fails, or if the message is not a serialized
|
||||||
|
// p2p.Msg (which it always will be if it is sent from this object).
|
||||||
|
func (self *Protocol) Handle(msg []byte, p *p2p.Peer, asymmetric bool, keyid string) error {
|
||||||
|
var vrw *PssReadWriter
|
||||||
|
if self.Asymmetric != asymmetric && self.Symmetric == !asymmetric {
|
||||||
|
return fmt.Errorf("invalid protocol encryption")
|
||||||
|
} else if (!self.isActiveSymKey(keyid, *self.topic) && !asymmetric) ||
|
||||||
|
(!self.isActiveAsymKey(keyid, *self.topic) && asymmetric) {
|
||||||
|
|
||||||
|
rw, err := self.AddPeer(p, self.proto.Run, *self.topic, asymmetric, keyid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vrw = rw.(*PssReadWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pmsg, err := ToP2pMsg(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not decode pssmsg")
|
||||||
|
}
|
||||||
|
if asymmetric {
|
||||||
|
vrw = self.pubKeyRWPool[keyid].(*PssReadWriter)
|
||||||
|
} else {
|
||||||
|
vrw = self.symKeyRWPool[keyid].(*PssReadWriter)
|
||||||
|
}
|
||||||
|
vrw.injectMsg(pmsg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if (peer) symmetric key is currently registered with this topic
|
||||||
|
func (self *Protocol) isActiveSymKey(key string, topic Topic) bool {
|
||||||
|
return self.symKeyRWPool[key] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if (peer) asymmetric key is currently registered with this topic
|
||||||
|
func (self *Protocol) isActiveAsymKey(key string, topic Topic) bool {
|
||||||
|
return self.pubKeyRWPool[key] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a serialized (non-buffered) version of a p2p.Msg, used in the specialized internal p2p.MsgReadwriter implementations
|
||||||
|
func ToP2pMsg(msg []byte) (p2p.Msg, error) {
|
||||||
|
payload := &ProtocolMsg{}
|
||||||
|
if err := rlp.DecodeBytes(msg, payload); err != nil {
|
||||||
|
return p2p.Msg{}, fmt.Errorf("pss protocol handler unable to decode payload as p2p message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p2p.Msg{
|
||||||
|
Code: payload.Code,
|
||||||
|
Size: uint32(len(payload.Payload)),
|
||||||
|
ReceivedAt: time.Now(),
|
||||||
|
Payload: bytes.NewBuffer(payload.Payload),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs an emulated pss Protocol on the specified peer,
|
||||||
|
// linked to a specific topic
|
||||||
|
// `key` and `asymmetric` specifies what encryption key
|
||||||
|
// to link the peer to.
|
||||||
|
// The key must exist in the pss store prior to adding the peer.
|
||||||
|
func (self *Protocol) AddPeer(p *p2p.Peer, run func(*p2p.Peer, p2p.MsgReadWriter) error, topic Topic, asymmetric bool, key string) (p2p.MsgReadWriter, error) {
|
||||||
|
self.Pss.lock.Lock()
|
||||||
|
defer self.Pss.lock.Unlock()
|
||||||
|
rw := &PssReadWriter{
|
||||||
|
Pss: self.Pss,
|
||||||
|
rw: make(chan p2p.Msg),
|
||||||
|
spec: self.spec,
|
||||||
|
topic: self.topic,
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
|
if asymmetric {
|
||||||
|
rw.sendFunc = self.Pss.SendAsym
|
||||||
|
} else {
|
||||||
|
rw.sendFunc = self.Pss.SendSym
|
||||||
|
}
|
||||||
|
if asymmetric {
|
||||||
|
if _, ok := self.Pss.pubKeyPool[key]; !ok {
|
||||||
|
return nil, fmt.Errorf("asym key does not exist: %s", key)
|
||||||
|
}
|
||||||
|
self.pubKeyRWPool[key] = rw
|
||||||
|
} else {
|
||||||
|
if _, ok := self.Pss.symKeyPool[key]; !ok {
|
||||||
|
return nil, fmt.Errorf("symkey does not exist: %s", key)
|
||||||
|
}
|
||||||
|
self.symKeyRWPool[key] = rw
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := run(p, rw)
|
||||||
|
log.Warn(fmt.Sprintf("pss vprotocol quit on addr %v topic %v: %v", topic, err))
|
||||||
|
}()
|
||||||
|
return rw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uniform translation of protocol specifiers to topic
|
||||||
|
func ProtocolTopic(spec *protocols.Spec) Topic {
|
||||||
|
return BytesToTopic([]byte(fmt.Sprintf("%s:%d", spec.Name, spec.Version)))
|
||||||
|
}
|
||||||
7
swarm/pss/protocol_none.go
Normal file
7
swarm/pss/protocol_none.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// +build nopssprotocol
|
||||||
|
|
||||||
|
package pss
|
||||||
|
|
||||||
|
const (
|
||||||
|
IsActiveProtocol = false
|
||||||
|
)
|
||||||
132
swarm/pss/protocol_test.go
Normal file
132
swarm/pss/protocol_test.go
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
)
|
||||||
|
|
||||||
|
type protoCtrl struct {
|
||||||
|
C chan bool
|
||||||
|
protocol *Protocol
|
||||||
|
run func(*p2p.Peer, p2p.MsgReadWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple ping pong protocol test for the pss devp2p emulation
|
||||||
|
func TestProtocol(t *testing.T) {
|
||||||
|
t.Run("32", testProtocol)
|
||||||
|
t.Run("8", testProtocol)
|
||||||
|
t.Run("0", testProtocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testProtocol(t *testing.T) {
|
||||||
|
|
||||||
|
// address hint size
|
||||||
|
var addrsize int64
|
||||||
|
paramstring := strings.Split(t.Name(), "/")
|
||||||
|
addrsize, _ = strconv.ParseInt(paramstring[1], 10, 0)
|
||||||
|
log.Info("protocol test", "addrsize", addrsize)
|
||||||
|
|
||||||
|
topic := PingTopic.String()
|
||||||
|
|
||||||
|
clients, err := setupNetwork(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var loaddrhex string
|
||||||
|
err = clients[0].Call(&loaddrhex, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
loaddrhex = loaddrhex[:2+(addrsize*2)]
|
||||||
|
var roaddrhex string
|
||||||
|
err = clients[1].Call(&roaddrhex, "pss_baseAddr")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 baseaddr fail: %v", err)
|
||||||
|
}
|
||||||
|
roaddrhex = roaddrhex[:2+(addrsize*2)]
|
||||||
|
lnodeinfo := &p2p.NodeInfo{}
|
||||||
|
err = clients[0].Call(&lnodeinfo, "admin_nodeInfo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc nodeinfo node 11 fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lpubkey string
|
||||||
|
err = clients[0].Call(&lpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 1 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
var rpubkey string
|
||||||
|
err = clients[1].Call(&rpubkey, "pss_getPublicKey")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rpc get node 2 pubkey fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 1000) // replace with hive healthy code
|
||||||
|
|
||||||
|
lmsgC := make(chan APIMsg)
|
||||||
|
lctx, _ := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
lsub, err := clients[0].Subscribe(lctx, "pss", lmsgC, "receive", topic)
|
||||||
|
defer lsub.Unsubscribe()
|
||||||
|
rmsgC := make(chan APIMsg)
|
||||||
|
rctx, _ := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
rsub, err := clients[1].Subscribe(rctx, "pss", rmsgC, "receive", topic)
|
||||||
|
defer rsub.Unsubscribe()
|
||||||
|
|
||||||
|
// set reciprocal public keys
|
||||||
|
err = clients[0].Call(nil, "pss_setPeerPublicKey", rpubkey, topic, roaddrhex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = clients[1].Call(nil, "pss_setPeerPublicKey", lpubkey, topic, loaddrhex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add right peer's public key as protocol peer on left
|
||||||
|
nid, _ := discover.HexID("0x00") // this hack is needed to satisfy the p2p method
|
||||||
|
p := p2p.NewPeer(nid, fmt.Sprintf("%x", common.FromHex(loaddrhex)), []p2p.Cap{})
|
||||||
|
_, err = pssprotocols[lnodeinfo.ID].protocol.AddPeer(p, pssprotocols[lnodeinfo.ID].run, PingTopic, true, rpubkey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends ping asym, checks delivery
|
||||||
|
pssprotocols[lnodeinfo.ID].C <- false
|
||||||
|
select {
|
||||||
|
case <-lmsgC:
|
||||||
|
log.Debug("lnode ok")
|
||||||
|
case cerr := <-lctx.Done():
|
||||||
|
t.Fatalf("test message timed out: %v", cerr)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-rmsgC:
|
||||||
|
log.Debug("rnode ok")
|
||||||
|
case cerr := <-lctx.Done():
|
||||||
|
t.Fatalf("test message timed out: %v", cerr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends ping asym, checks delivery
|
||||||
|
pssprotocols[lnodeinfo.ID].C <- false
|
||||||
|
select {
|
||||||
|
case <-lmsgC:
|
||||||
|
log.Debug("lnode ok")
|
||||||
|
case cerr := <-lctx.Done():
|
||||||
|
t.Fatalf("test message timed out: %v", cerr)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-rmsgC:
|
||||||
|
log.Debug("rnode ok")
|
||||||
|
case cerr := <-lctx.Done():
|
||||||
|
t.Fatalf("test message timed out: %v", cerr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
774
swarm/pss/pss.go
Normal file
774
swarm/pss/pss.go
Normal file
|
|
@ -0,0 +1,774 @@
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/protocols"
|
||||||
|
"github.com/ethereum/go-ethereum/pot"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/network"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: proper padding generation for messages
|
||||||
|
const (
|
||||||
|
defaultPaddingByteSize = 16
|
||||||
|
defaultMsgTTL = time.Second * 8
|
||||||
|
defaultDigestCacheTTL = time.Second
|
||||||
|
defaultSymKeyCacheCapacity = 512
|
||||||
|
digestLength = 32 // byte length of digest used for pss cache (currently same as swarm chunk hash)
|
||||||
|
defaultWhisperWorkTime = 3
|
||||||
|
defaultWhisperPoW = 0.0000000001
|
||||||
|
defaultMaxMsgSize = 1024 * 1024
|
||||||
|
defaultCleanInterval = 1000 * 60 * 10
|
||||||
|
pssProtocolName = "pss"
|
||||||
|
pssVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
addressLength = len(pot.Address{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// cache is used for preventing backwards routing
|
||||||
|
// will also be instrumental in flood guard mechanism
|
||||||
|
// and mailbox implementation
|
||||||
|
type pssCacheEntry struct {
|
||||||
|
expiresAt time.Time
|
||||||
|
receivedFrom []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// abstraction to enable access to p2p.protocols.Peer.Send
|
||||||
|
type senderPeer interface {
|
||||||
|
Info() *p2p.PeerInfo
|
||||||
|
ID() discover.NodeID
|
||||||
|
Address() []byte
|
||||||
|
Send(interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// per-key peer related information
|
||||||
|
// member `protected` prevents garbage collection of the instance
|
||||||
|
type pssPeer struct {
|
||||||
|
lastSeen time.Time
|
||||||
|
address *PssAddress
|
||||||
|
protected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pss configuration parameters
|
||||||
|
type PssParams struct {
|
||||||
|
MsgTTL time.Duration
|
||||||
|
CacheTTL time.Duration
|
||||||
|
privateKey *ecdsa.PrivateKey
|
||||||
|
SymKeyCacheCapacity int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sane defaults for Pss
|
||||||
|
func NewPssParams(privatekey *ecdsa.PrivateKey) *PssParams {
|
||||||
|
return &PssParams{
|
||||||
|
MsgTTL: defaultMsgTTL,
|
||||||
|
CacheTTL: defaultDigestCacheTTL,
|
||||||
|
privateKey: privatekey,
|
||||||
|
SymKeyCacheCapacity: defaultSymKeyCacheCapacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toplevel pss object, takes care of message sending, receiving, decryption and encryption, message handler dispatchers and message forwarding.
|
||||||
|
//
|
||||||
|
// Implements node.Service
|
||||||
|
type Pss struct {
|
||||||
|
network.Overlay // we can get the overlayaddress from this
|
||||||
|
privateKey *ecdsa.PrivateKey // pss can have it's own independent key
|
||||||
|
dpa *storage.DPA // we use swarm to store the cache
|
||||||
|
w *whisper.Whisper // key and encryption backend
|
||||||
|
auxAPIs []rpc.API // builtins (handshake, test) can add APIs
|
||||||
|
|
||||||
|
// sending and forwarding
|
||||||
|
fwdPool map[string]*protocols.Peer // keep track of all peers sitting on the pssmsg routing layer
|
||||||
|
fwdCache map[pssDigest]pssCacheEntry // checksum of unique fields from pssmsg mapped to expiry, cache to determine whether to drop msg
|
||||||
|
cacheTTL time.Duration // how long to keep messages in fwdCache (not implemented)
|
||||||
|
msgTTL time.Duration
|
||||||
|
paddingByteSize int
|
||||||
|
capstring string
|
||||||
|
|
||||||
|
// keys and peers
|
||||||
|
pubKeyPool map[string]map[Topic]*pssPeer // mapping of hex public keys to peer address by topic.
|
||||||
|
symKeyPool map[string]map[Topic]*pssPeer // mapping of symkeyids to peer address by topic.
|
||||||
|
symKeyDecryptCache []*string // fast lookup of symkeys recently used for decryption; last used is on top of stack
|
||||||
|
symKeyDecryptCacheCursor int // modular cursor pointing to last used, wraps on symKeyDecryptCache array
|
||||||
|
symKeyDecryptCacheCapacity int // max amount of symkeys to keep.
|
||||||
|
|
||||||
|
// message handling
|
||||||
|
handlers map[Topic]map[*Handler]bool // topic and version based pss payload handlers. See pss.Handle()
|
||||||
|
|
||||||
|
// process
|
||||||
|
lock sync.Mutex
|
||||||
|
quitC chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Pss) String() string {
|
||||||
|
return fmt.Sprintf("pss: addr %x, pubkey %v", self.BaseAddr(), common.ToHex(crypto.FromECDSAPub(&self.privateKey.PublicKey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new Pss instance.
|
||||||
|
//
|
||||||
|
// In addition to params, it takes a swarm network overlay
|
||||||
|
// and a DPA storage for message cache storage.
|
||||||
|
func NewPss(k network.Overlay, dpa *storage.DPA, params *PssParams) *Pss {
|
||||||
|
cap := p2p.Cap{
|
||||||
|
Name: pssProtocolName,
|
||||||
|
Version: pssVersion,
|
||||||
|
}
|
||||||
|
return &Pss{
|
||||||
|
Overlay: k,
|
||||||
|
privateKey: params.privateKey,
|
||||||
|
dpa: dpa,
|
||||||
|
w: whisper.New(&whisper.DefaultConfig),
|
||||||
|
quitC: make(chan struct{}),
|
||||||
|
|
||||||
|
fwdPool: make(map[string]*protocols.Peer),
|
||||||
|
fwdCache: make(map[pssDigest]pssCacheEntry),
|
||||||
|
cacheTTL: params.CacheTTL,
|
||||||
|
msgTTL: params.MsgTTL,
|
||||||
|
paddingByteSize: defaultPaddingByteSize,
|
||||||
|
capstring: cap.String(),
|
||||||
|
|
||||||
|
pubKeyPool: make(map[string]map[Topic]*pssPeer),
|
||||||
|
symKeyPool: make(map[string]map[Topic]*pssPeer),
|
||||||
|
symKeyDecryptCache: make([]*string, params.SymKeyCacheCapacity),
|
||||||
|
symKeyDecryptCacheCapacity: params.SymKeyCacheCapacity,
|
||||||
|
|
||||||
|
handlers: make(map[Topic]map[*Handler]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// SECTION: node.Service interface
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func (self *Pss) Start(srv *p2p.Server) error {
|
||||||
|
go func() {
|
||||||
|
tickC := time.Tick(defaultCleanInterval)
|
||||||
|
select {
|
||||||
|
case <-tickC:
|
||||||
|
self.cleanKeys()
|
||||||
|
case <-self.quitC:
|
||||||
|
log.Info("pss shutting down")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Debug("Started pss", "public key", common.ToHex(crypto.FromECDSAPub(self.PublicKey())))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Pss) Stop() error {
|
||||||
|
close(self.quitC)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pssSpec = &protocols.Spec{
|
||||||
|
Name: pssProtocolName,
|
||||||
|
Version: pssVersion,
|
||||||
|
MaxMsgSize: defaultMaxMsgSize,
|
||||||
|
Messages: []interface{}{
|
||||||
|
PssMsg{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Pss) Protocols() []p2p.Protocol {
|
||||||
|
return []p2p.Protocol{
|
||||||
|
p2p.Protocol{
|
||||||
|
Name: pssSpec.Name,
|
||||||
|
Version: pssSpec.Version,
|
||||||
|
Length: pssSpec.Length(),
|
||||||
|
Run: self.Run,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Pss) Run(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||||
|
pp := protocols.NewPeer(p, rw, pssSpec)
|
||||||
|
self.fwdPool[p.Info().ID] = pp
|
||||||
|
return pp.Run(self.handlePssMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Pss) APIs() []rpc.API {
|
||||||
|
apis := []rpc.API{
|
||||||
|
rpc.API{
|
||||||
|
Namespace: "pss",
|
||||||
|
Version: "1.0",
|
||||||
|
Service: NewAPI(self),
|
||||||
|
Public: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, auxapi := range self.auxAPIs {
|
||||||
|
apis = append(apis, auxapi)
|
||||||
|
}
|
||||||
|
return apis
|
||||||
|
}
|
||||||
|
|
||||||
|
// add API methods to the pss API
|
||||||
|
// must be run before node is started
|
||||||
|
func (self *Pss) addAPI(api rpc.API) {
|
||||||
|
self.auxAPIs = append(self.auxAPIs, api)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the swarm overlay address of the pss node
|
||||||
|
func (self *Pss) BaseAddr() []byte {
|
||||||
|
return self.Overlay.BaseAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the pss node's public key
|
||||||
|
func (self *Pss) PublicKey() *ecdsa.PublicKey {
|
||||||
|
return &self.privateKey.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// SECTION: Message handling
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Links a handler function to a Topic
|
||||||
|
//
|
||||||
|
// All incoming messages with an envelope Topic matching the
|
||||||
|
// topic specified will be passed to the given Handler function.
|
||||||
|
//
|
||||||
|
// There may be an arbitrary number of handler functions per topic.
|
||||||
|
//
|
||||||
|
// Returns a deregister function which needs to be called to
|
||||||
|
// deregister the handler,
|
||||||
|
func (self *Pss) Register(topic *Topic, handler Handler) func() {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
handlers := self.handlers[*topic]
|
||||||
|
if handlers == nil {
|
||||||
|
handlers = make(map[*Handler]bool)
|
||||||
|
self.handlers[*topic] = handlers
|
||||||
|
}
|
||||||
|
handlers[&handler] = true
|
||||||
|
return func() { self.deregister(topic, &handler) }
|
||||||
|
}
|
||||||
|
func (self *Pss) deregister(topic *Topic, h *Handler) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
handlers := self.handlers[*topic]
|
||||||
|
if len(handlers) == 1 {
|
||||||
|
delete(self.handlers, *topic)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(handlers, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all registered handlers for respective topics
|
||||||
|
func (self *Pss) getHandlers(topic Topic) map[*Handler]bool {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
return self.handlers[topic]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters incoming messages for processing or forwarding.
|
||||||
|
// Check if address partially matches
|
||||||
|
// If yes, it CAN be for us, and we process it
|
||||||
|
// Passes error to pss protocol handler if payload is not valid pssmsg
|
||||||
|
func (self *Pss) handlePssMsg(msg interface{}) error {
|
||||||
|
pssmsg, ok := msg.(*PssMsg)
|
||||||
|
if ok {
|
||||||
|
var err error
|
||||||
|
if !self.isSelfPossibleRecipient(pssmsg) {
|
||||||
|
msgexp := time.Unix(int64(pssmsg.Expire), 0)
|
||||||
|
if msgexp.Before(time.Now()) {
|
||||||
|
log.Trace("pss expired :/ ... dropping")
|
||||||
|
return nil
|
||||||
|
} else if msgexp.After(time.Now().Add(self.msgTTL)) {
|
||||||
|
return errors.New("Invalid TTL")
|
||||||
|
}
|
||||||
|
log.Trace("pss was for someone else :'( ... forwarding", "pss", common.ToHex(self.BaseAddr()))
|
||||||
|
return self.forward(pssmsg)
|
||||||
|
}
|
||||||
|
log.Trace("pss for us, yay! ... let's process!", "pss", common.ToHex(self.BaseAddr()))
|
||||||
|
|
||||||
|
if !self.process(pssmsg) {
|
||||||
|
err = self.forward(pssmsg)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid message type. Expected *PssMsg, got %T ", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point to processing a message for which the current node can be the intended recipient.
|
||||||
|
// Attempts symmetric and asymmetric decryption with stored keys.
|
||||||
|
// Dispatches message to all handlers matching the message topic
|
||||||
|
func (self *Pss) process(pssmsg *PssMsg) bool {
|
||||||
|
var err error
|
||||||
|
var recvmsg *whisper.ReceivedMessage
|
||||||
|
var from *PssAddress
|
||||||
|
var asymmetric bool
|
||||||
|
var keyid string
|
||||||
|
var keyFunc func(envelope *whisper.Envelope) (*whisper.ReceivedMessage, string, *PssAddress, error)
|
||||||
|
|
||||||
|
envelope := pssmsg.Payload
|
||||||
|
psstopic := Topic(envelope.Topic)
|
||||||
|
|
||||||
|
if len(envelope.AESNonce) > 0 { // detect symkey msg according to whisperv5/envelope.go:OpenSymmetric
|
||||||
|
keyFunc = self.processSym
|
||||||
|
} else {
|
||||||
|
asymmetric = true
|
||||||
|
keyFunc = self.processAsym
|
||||||
|
}
|
||||||
|
recvmsg, keyid, from, err = keyFunc(envelope)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("decrypt message fail", "err", err, "asym", asymmetric, "pss", common.ToHex(self.BaseAddr()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pssmsg.To) < addressLength {
|
||||||
|
go func() {
|
||||||
|
err := self.forward(pssmsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Redundant forward fail: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
handlers := self.getHandlers(psstopic)
|
||||||
|
nid, _ := discover.HexID("0x00") // this hack is needed to satisfy the p2p method
|
||||||
|
p := p2p.NewPeer(nid, fmt.Sprintf("%x", from), []p2p.Cap{})
|
||||||
|
for f := range handlers {
|
||||||
|
err := (*f)(recvmsg.Payload, p, asymmetric, keyid)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Pss handler %p failed: %v", f, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// will return false if using partial address
|
||||||
|
func (self *Pss) isSelfRecipient(msg *PssMsg) bool {
|
||||||
|
return bytes.Equal(msg.To, self.Overlay.BaseAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// test match of leftmost bytes in given message to node's overlay address
|
||||||
|
func (self *Pss) isSelfPossibleRecipient(msg *PssMsg) bool {
|
||||||
|
local := self.Overlay.BaseAddr()
|
||||||
|
return bytes.Equal(msg.To[:], local[:len(msg.To)])
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// SECTION: Encryption
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Links a peer ECDSA public key to a topic
|
||||||
|
//
|
||||||
|
// This is required for asymmetric message exchange
|
||||||
|
// on the given topic
|
||||||
|
//
|
||||||
|
// The value in `address` will be used as a routing hint for the
|
||||||
|
// public key / topic association
|
||||||
|
func (self *Pss) SetPeerPublicKey(pubkey *ecdsa.PublicKey, topic Topic, address *PssAddress) error {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
pubkeybytes := crypto.FromECDSAPub(pubkey)
|
||||||
|
if len(pubkeybytes) == 0 {
|
||||||
|
return fmt.Errorf("invalid public key: %v", pubkey)
|
||||||
|
}
|
||||||
|
pubkeyid := common.ToHex(pubkeybytes)
|
||||||
|
psp := &pssPeer{
|
||||||
|
address: address,
|
||||||
|
}
|
||||||
|
if _, ok := self.pubKeyPool[pubkeyid]; ok == false {
|
||||||
|
self.pubKeyPool[pubkeyid] = make(map[Topic]*pssPeer)
|
||||||
|
}
|
||||||
|
self.pubKeyPool[pubkeyid][topic] = psp
|
||||||
|
log.Trace("added pubkey", "pubkeyid", pubkeyid, "topic", topic, "address", common.ToHex(*address))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically generate a new symkey for a topic and address hint
|
||||||
|
func (self *Pss) generateSymmetricKey(topic Topic, address *PssAddress, addToCache bool) (string, error) {
|
||||||
|
keyid, err := self.w.GenerateSymKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
self.addSymmetricKeyToPool(keyid, topic, address, addToCache)
|
||||||
|
return keyid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links a peer symmetric key (arbitrary byte sequence) to a topic
|
||||||
|
//
|
||||||
|
// This is required for symmetrically encrypted message exchange
|
||||||
|
// on the given topic
|
||||||
|
//
|
||||||
|
// The key is stored in the whisper backend.
|
||||||
|
//
|
||||||
|
// If addtocache is set to true, the key will be added to the cache of keys
|
||||||
|
// used to attempt symmetric decryption of incoming messages.
|
||||||
|
//
|
||||||
|
// Returns a string id that can be used to retreive the key bytes
|
||||||
|
// from the whisper backend (see pss.GetSymmetricKey())
|
||||||
|
func (self *Pss) SetSymmetricKey(key []byte, topic Topic, address *PssAddress, addtocache bool) (string, error) {
|
||||||
|
keyid, err := self.w.AddSymKeyDirect(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
self.addSymmetricKeyToPool(keyid, topic, address, addtocache)
|
||||||
|
return keyid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds a symmetric key to the pss key pool, and optionally adds the key
|
||||||
|
// to the collection of keys used to attempt symmetric decryption of
|
||||||
|
// incoming messages
|
||||||
|
func (self *Pss) addSymmetricKeyToPool(keyid string, topic Topic, address *PssAddress, addtocache bool) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
psp := &pssPeer{
|
||||||
|
address: address,
|
||||||
|
}
|
||||||
|
if _, ok := self.symKeyPool[keyid]; !ok {
|
||||||
|
self.symKeyPool[keyid] = make(map[Topic]*pssPeer)
|
||||||
|
}
|
||||||
|
self.symKeyPool[keyid][topic] = psp
|
||||||
|
if addtocache {
|
||||||
|
self.symKeyDecryptCacheCursor++
|
||||||
|
self.symKeyDecryptCache[self.symKeyDecryptCacheCursor%cap(self.symKeyDecryptCache)] = &keyid
|
||||||
|
}
|
||||||
|
key, _ := self.GetSymmetricKey(keyid)
|
||||||
|
log.Trace("added symkey", "symkeyid", keyid, "symkey", common.ToHex(key), "topic", topic, "address", fmt.Sprintf("%p", address), "cache", addtocache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a symmetric key byte seqyence stored in the whisper backend
|
||||||
|
// by its unique id
|
||||||
|
//
|
||||||
|
// Passes on the error value from the whisper backend
|
||||||
|
func (self *Pss) GetSymmetricKey(symkeyid string) ([]byte, error) {
|
||||||
|
symkey, err := self.w.GetSymKey(symkeyid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return symkey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to decrypt, validate and unpack a
|
||||||
|
// symmetrically encrypted message
|
||||||
|
// If successful, returns the unpacked whisper ReceivedMessage struct
|
||||||
|
// encapsulating the decrypted message, and the whisper backend id
|
||||||
|
// of the symmetric key used to decrypt the message.
|
||||||
|
// It fails if decryption of the message fails or if the message is corrupted
|
||||||
|
func (self *Pss) processSym(envelope *whisper.Envelope) (*whisper.ReceivedMessage, string, *PssAddress, error) {
|
||||||
|
for i := self.symKeyDecryptCacheCursor; i > self.symKeyDecryptCacheCursor-cap(self.symKeyDecryptCache) && i > 0; i-- {
|
||||||
|
symkeyid := self.symKeyDecryptCache[i%cap(self.symKeyDecryptCache)]
|
||||||
|
symkey, err := self.w.GetSymKey(*symkeyid)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
recvmsg, err := envelope.OpenSymmetric(symkey)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !recvmsg.Validate() {
|
||||||
|
return nil, "", nil, fmt.Errorf("symmetrically encrypted message has invalid signature or is corrupt")
|
||||||
|
}
|
||||||
|
from := self.symKeyPool[*symkeyid][Topic(envelope.Topic)].address
|
||||||
|
self.symKeyDecryptCacheCursor++
|
||||||
|
self.symKeyDecryptCache[self.symKeyDecryptCacheCursor%cap(self.symKeyDecryptCache)] = symkeyid
|
||||||
|
return recvmsg, *symkeyid, from, nil
|
||||||
|
}
|
||||||
|
return nil, "", nil, fmt.Errorf("could not decrypt message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to decrypt, validate and unpack an
|
||||||
|
// asymmetrically encrypted message
|
||||||
|
// If successful, returns the unpacked whisper ReceivedMessage struct
|
||||||
|
// encapsulating the decrypted message, and the byte representation of
|
||||||
|
// the public key used to decrypt the message.
|
||||||
|
// It fails if decryption of message fails, or if the message is corrupted
|
||||||
|
func (self *Pss) processAsym(envelope *whisper.Envelope) (*whisper.ReceivedMessage, string, *PssAddress, error) {
|
||||||
|
recvmsg, err := envelope.OpenAsymmetric(self.privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, fmt.Errorf("could not decrypt message: %v", "err", err)
|
||||||
|
}
|
||||||
|
// check signature (if signed), strip padding
|
||||||
|
if !recvmsg.Validate() {
|
||||||
|
return nil, "", nil, fmt.Errorf("invalid message")
|
||||||
|
}
|
||||||
|
pubkeyid := common.ToHex(crypto.FromECDSAPub(recvmsg.Src))
|
||||||
|
var from *PssAddress
|
||||||
|
if self.pubKeyPool[pubkeyid][Topic(envelope.Topic)] != nil {
|
||||||
|
from = self.pubKeyPool[pubkeyid][Topic(envelope.Topic)].address
|
||||||
|
}
|
||||||
|
return recvmsg, pubkeyid, from, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symkey garbage collection
|
||||||
|
// a key is removed if:
|
||||||
|
// - it is not marked as protected
|
||||||
|
// - it is not in the incoming decryption cache
|
||||||
|
func (self *Pss) cleanKeys() (count int) {
|
||||||
|
for keyid, peertopics := range self.symKeyPool {
|
||||||
|
var expiredtopics []Topic
|
||||||
|
for topic, psp := range peertopics {
|
||||||
|
log.Trace("check topic", "topic", topic, "id", keyid, "protect", psp.protected, "p", fmt.Sprintf("%p", self.symKeyPool[keyid][topic]))
|
||||||
|
if psp.protected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var match bool
|
||||||
|
for i := self.symKeyDecryptCacheCursor; i > self.symKeyDecryptCacheCursor-cap(self.symKeyDecryptCache) && i > 0; i-- {
|
||||||
|
cacheid := self.symKeyDecryptCache[i%cap(self.symKeyDecryptCache)]
|
||||||
|
log.Trace("check cache", "idx", i, "id", *cacheid)
|
||||||
|
if *cacheid == keyid {
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match == false {
|
||||||
|
expiredtopics = append(expiredtopics, topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, topic := range expiredtopics {
|
||||||
|
delete(self.symKeyPool[keyid], topic)
|
||||||
|
log.Trace("symkey cleanup deletion", "symkeyid", keyid, "topic", topic, "val", self.symKeyPool[keyid])
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// SECTION: Message sending
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Send a message using symmetric encryption
|
||||||
|
//
|
||||||
|
// Fails if the key id does not match any of the stored symmetric keys
|
||||||
|
func (self *Pss) SendSym(symkeyid string, topic Topic, msg []byte) error {
|
||||||
|
symkey, err := self.GetSymmetricKey(symkeyid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("missing valid send symkey %s: %v", symkeyid, err)
|
||||||
|
}
|
||||||
|
psp, ok := self.symKeyPool[symkeyid][topic]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid topic '%s' for symkey '%s'", topic, symkeyid)
|
||||||
|
} else if psp.address == nil {
|
||||||
|
return fmt.Errorf("no address hint for topic '%s' symkey '%s'", topic, symkeyid)
|
||||||
|
}
|
||||||
|
err = self.send(*psp.address, topic, msg, false, symkey)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message using asymmetric encryption
|
||||||
|
//
|
||||||
|
// Fails if the key id does not match any in of the stored public keys
|
||||||
|
func (self *Pss) SendAsym(pubkeyid string, topic Topic, msg []byte) error {
|
||||||
|
//pubkey := self.pubKeyIndex[pubkeyid]
|
||||||
|
pubkey := crypto.ToECDSAPub(common.FromHex(pubkeyid))
|
||||||
|
if pubkey == nil {
|
||||||
|
return fmt.Errorf("Invalid public key id %x", pubkey)
|
||||||
|
}
|
||||||
|
psp, ok := self.pubKeyPool[pubkeyid][topic]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid topic '%s' for pubkey '%s'", topic, pubkeyid)
|
||||||
|
} else if psp.address == nil {
|
||||||
|
return fmt.Errorf("no address hint for topic '%s' pubkey '%s'", topic, pubkeyid)
|
||||||
|
}
|
||||||
|
self.send(*psp.address, topic, msg, true, common.FromHex(pubkeyid))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send is payload agnostic, and will accept any byte slice as payload
|
||||||
|
// It generates an whisper envelope for the specified recipient and topic,
|
||||||
|
// and wraps the message payload in it.
|
||||||
|
// TODO: Implement proper message padding
|
||||||
|
func (self *Pss) send(to []byte, topic Topic, msg []byte, asymmetric bool, key []byte) error {
|
||||||
|
if key == nil || bytes.Equal(key, []byte{}) {
|
||||||
|
return fmt.Errorf("Zero length key passed to pss send")
|
||||||
|
}
|
||||||
|
padding := make([]byte, self.paddingByteSize)
|
||||||
|
c, err := rand.Read(padding)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if c < self.paddingByteSize {
|
||||||
|
return fmt.Errorf("invalid padding length: %d", c)
|
||||||
|
}
|
||||||
|
wparams := &whisper.MessageParams{
|
||||||
|
TTL: defaultWhisperTTL,
|
||||||
|
Src: self.privateKey,
|
||||||
|
Topic: whisper.TopicType(topic),
|
||||||
|
WorkTime: defaultWhisperWorkTime,
|
||||||
|
PoW: defaultWhisperPoW,
|
||||||
|
Payload: msg,
|
||||||
|
Padding: padding,
|
||||||
|
}
|
||||||
|
if asymmetric {
|
||||||
|
wparams.Dst = crypto.ToECDSAPub(key)
|
||||||
|
} else {
|
||||||
|
wparams.KeySym = key
|
||||||
|
}
|
||||||
|
// set up outgoing message container, which does encryption and envelope wrapping
|
||||||
|
woutmsg, err := whisper.NewSentMessage(wparams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate whisper message encapsulation: %v", err)
|
||||||
|
}
|
||||||
|
// performs encryption.
|
||||||
|
// Does NOT perform / performs negligible PoW due to very low difficulty setting
|
||||||
|
// after this the message is ready for sending
|
||||||
|
envelope, err := woutmsg.Wrap(wparams)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform whisper encryption: %v", err)
|
||||||
|
}
|
||||||
|
log.Trace("pssmsg whisper done", "env", envelope, "wparams payload", common.ToHex(wparams.Payload), "to", common.ToHex(to), "asym", asymmetric, "key", common.ToHex(key))
|
||||||
|
// prepare for devp2p transport
|
||||||
|
pssmsg := &PssMsg{
|
||||||
|
To: to,
|
||||||
|
Expire: uint32(time.Now().Add(self.msgTTL).Unix()),
|
||||||
|
Payload: envelope,
|
||||||
|
}
|
||||||
|
return self.forward(pssmsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forwards a pss message to the peer(s) closest to the to recipient address in the PssMsg struct
|
||||||
|
// The recipient address can be of any length, and the byte slice will be matched to the MSB slice
|
||||||
|
// of the peer address of the equivalent length.
|
||||||
|
func (self *Pss) forward(msg *PssMsg) error {
|
||||||
|
to := make([]byte, addressLength)
|
||||||
|
copy(to[:len(msg.To)], msg.To)
|
||||||
|
|
||||||
|
// cache the message
|
||||||
|
digest, err := self.storeMsg(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(fmt.Sprintf("could not store message %v to cache: %v", msg, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// flood guard:
|
||||||
|
// don't allow identical messages we saw shortly before
|
||||||
|
if self.checkFwdCache(nil, digest) {
|
||||||
|
log.Trace(fmt.Sprintf("pss relay block-cache match: FROM %x TO %x", self.Overlay.BaseAddr(), common.ToHex(msg.To)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// send with kademlia
|
||||||
|
// find the closest peer to the recipient and attempt to send
|
||||||
|
sent := 0
|
||||||
|
|
||||||
|
self.Overlay.EachConn(to, 256, func(op network.OverlayConn, po int, isproxbin bool) bool {
|
||||||
|
// we need p2p.protocols.Peer.Send
|
||||||
|
// cast and resolve
|
||||||
|
sp, ok := op.(senderPeer)
|
||||||
|
if !ok {
|
||||||
|
log.Crit("Pss cannot use kademlia peer type")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
info := sp.Info()
|
||||||
|
|
||||||
|
// check if the peer is running pss
|
||||||
|
var ispss bool
|
||||||
|
for _, cap := range info.Caps {
|
||||||
|
if cap == self.capstring {
|
||||||
|
ispss = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ispss {
|
||||||
|
log.Trace("peer doesn't have matching pss capabilities, skipping", "peer", info.Name, "caps", info.Caps)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the protocol peer from the forwarding peer cache
|
||||||
|
sendMsg := fmt.Sprintf("MSG %x TO %x FROM %x VIA %x", digest, to, self.BaseAddr(), op.Address())
|
||||||
|
pp := self.fwdPool[sp.Info().ID]
|
||||||
|
if self.checkFwdCache(op.Address(), digest) {
|
||||||
|
log.Trace(fmt.Sprintf("%v: peer already forwarded to", sendMsg))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// attempt to send the message
|
||||||
|
err := pp.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug(fmt.Sprintf("%v: failed forwarding: %v", sendMsg, err))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Trace(fmt.Sprintf("%v: successfully forwarded", sendMsg))
|
||||||
|
sent++
|
||||||
|
// continue forwarding if:
|
||||||
|
// - if the peer is end recipient but the full address has not been disclosed
|
||||||
|
// - if the peer address matches the partial address fully
|
||||||
|
// - if the peer is in proxbin
|
||||||
|
if len(msg.To) < addressLength && bytes.Equal(msg.To, op.Address()[:len(msg.To)]) {
|
||||||
|
log.Trace(fmt.Sprintf("Pss keep forwarding: Partial address + full partial match"))
|
||||||
|
return true
|
||||||
|
} else if isproxbin {
|
||||||
|
log.Trace(fmt.Sprintf("%x is in proxbin, keep forwarding", common.ToHex(op.Address())))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// at this point we stop forwarding, and the state is as follows:
|
||||||
|
// - the peer is end recipient and we have full address
|
||||||
|
// - we are not in proxbin (directed routing)
|
||||||
|
// - partial addresses don't fully match
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if sent == 0 {
|
||||||
|
log.Debug("unable to forward to any peers")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addFwdCache(digest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// SECTION: Caching
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// add a message to the cache
|
||||||
|
func (self *Pss) addFwdCache(digest pssDigest) error {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
var entry pssCacheEntry
|
||||||
|
var ok bool
|
||||||
|
if entry, ok = self.fwdCache[digest]; !ok {
|
||||||
|
entry = pssCacheEntry{}
|
||||||
|
}
|
||||||
|
entry.expiresAt = time.Now().Add(self.cacheTTL)
|
||||||
|
self.fwdCache[digest] = entry
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if message is in the cache
|
||||||
|
func (self *Pss) checkFwdCache(addr []byte, digest pssDigest) bool {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
entry, ok := self.fwdCache[digest]
|
||||||
|
if ok {
|
||||||
|
if entry.expiresAt.After(time.Now()) {
|
||||||
|
log.Trace(fmt.Sprintf("unexpired cache for digest %x", digest))
|
||||||
|
return true
|
||||||
|
} else if entry.expiresAt.IsZero() && bytes.Equal(addr, entry.receivedFrom) {
|
||||||
|
log.Trace(fmt.Sprintf("sendermatch %x for digest %x", common.ToHex(addr), digest))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DPA storage handler for message cache
|
||||||
|
func (self *Pss) storeMsg(msg *PssMsg) (pssDigest, error) {
|
||||||
|
swg := &sync.WaitGroup{}
|
||||||
|
wwg := &sync.WaitGroup{}
|
||||||
|
buf := bytes.NewReader(msg.serialize())
|
||||||
|
key, err := self.dpa.Store(buf, int64(buf.Len()), swg, wwg)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not store in swarm", "err", err)
|
||||||
|
return pssDigest{}, err
|
||||||
|
}
|
||||||
|
log.Trace("Stored msg in swarm", "key", key)
|
||||||
|
digest := pssDigest{}
|
||||||
|
copy(digest[:], key[:digestLength])
|
||||||
|
return digest, nil
|
||||||
|
}
|
||||||
1267
swarm/pss/pss_test.go
Normal file
1267
swarm/pss/pss_test.go
Normal file
File diff suppressed because it is too large
Load diff
3
swarm/pss/testdata/addpsstodiscoverytestsnapshot.sh
vendored
Normal file
3
swarm/pss/testdata/addpsstodiscoverytestsnapshot.sh
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sed -e 's/\(\"services\"\):\["discovery\"]/\1:["pss","bzz"]/'
|
||||||
1
swarm/pss/testdata/snapshot_128.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_128.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
swarm/pss/testdata/snapshot_16.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_16.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
swarm/pss/testdata/snapshot_256.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_256.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
swarm/pss/testdata/snapshot_32.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_32.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
swarm/pss/testdata/snapshot_64.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_64.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
swarm/pss/testdata/snapshot_8.json
vendored
Normal file
1
swarm/pss/testdata/snapshot_8.json
vendored
Normal file
File diff suppressed because one or more lines are too long
114
swarm/pss/types.go
Normal file
114
swarm/pss/types.go
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
package pss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/ethereum/go-ethereum/swarm/storage"
|
||||||
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultWhisperTTL = 6000
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
topicHashMutex = sync.Mutex{}
|
||||||
|
topicHashFunc = storage.MakeHashFunc("SHA256")()
|
||||||
|
)
|
||||||
|
|
||||||
|
type Topic whisper.TopicType
|
||||||
|
|
||||||
|
func (t *Topic) Unmarshal(input []byte) error {
|
||||||
|
err := hexutil.UnmarshalFixedText("Topic", input, t[:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Topic) String() string {
|
||||||
|
return hexutil.Encode(t[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Topic) MarshalJSON() (b []byte, err error) {
|
||||||
|
return json.Marshal(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Topic) UnmarshalJSON(input []byte) error {
|
||||||
|
topicbytes, err := hexutil.Decode(string(input[1 : len(input)-1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(t[:], topicbytes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// variable length address
|
||||||
|
type PssAddress []byte
|
||||||
|
|
||||||
|
func (a PssAddress) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(hexutil.Encode(a[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *PssAddress) UnmarshalJSON(input []byte) error {
|
||||||
|
b, err := hexutil.Decode(string(input[1 : len(input)-1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, bb := range b {
|
||||||
|
*a = append(*a, bb)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pssDigest [digestLength]byte
|
||||||
|
|
||||||
|
// Encapsulates messages transported over pss.
|
||||||
|
type PssMsg struct {
|
||||||
|
To []byte
|
||||||
|
Expire uint32
|
||||||
|
Payload *whisper.Envelope
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializes the message for use in cache
|
||||||
|
func (msg *PssMsg) serialize() []byte {
|
||||||
|
rlpdata, _ := rlp.EncodeToBytes(msg)
|
||||||
|
return rlpdata
|
||||||
|
}
|
||||||
|
|
||||||
|
// String representation of PssMsg
|
||||||
|
func (self *PssMsg) String() string {
|
||||||
|
return fmt.Sprintf("PssMsg: Recipient: %x", common.ToHex(self.To))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature for a message handler function for a PssMsg
|
||||||
|
//
|
||||||
|
// Implementations of this type are passed to Pss.Register together with a topic,
|
||||||
|
type Handler func(msg []byte, p *p2p.Peer, asymmetric bool, keyid string) error
|
||||||
|
|
||||||
|
type stateStore struct {
|
||||||
|
values map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStateStore() *stateStore {
|
||||||
|
return &stateStore{values: make(map[string][]byte)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *stateStore) Load(key string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *stateStore) Save(key string, v []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BytesToTopic(b []byte) Topic {
|
||||||
|
topicHashMutex.Lock()
|
||||||
|
defer topicHashMutex.Unlock()
|
||||||
|
topicHashFunc.Reset()
|
||||||
|
topicHashFunc.Write(b)
|
||||||
|
return Topic(whisper.BytesToTopic(topicHashFunc.Sum(nil)))
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue