From 193bc09c807936bbcb7b484d9bdbd780f463e3bd Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 22 Oct 2019 16:47:42 +0200 Subject: [PATCH] discv5: expand topic index description (#120) This change adds more text about the topic subsystem and changes the waiting time algorithm to ensure registration is resilient against the recently discovered ticket hoarding attack. There is still no simple and nice solution for radius estimation. --- discv5/discv5-rationale.md | 98 ++++++---- discv5/discv5-theory.md | 288 +++++++++++++++++----------- discv5/discv5-wire.md | 71 +++---- discv5/discv5.md | 2 +- discv5/img/ticket-validity.png | Bin 0 -> 13800 bytes discv5/img/topic-queue-diagram.png | Bin 0 -> 5816 bytes discv5/img/topic-radius-diagram.png | Bin 0 -> 10533 bytes 7 files changed, 260 insertions(+), 199 deletions(-) create mode 100644 discv5/img/ticket-validity.png create mode 100644 discv5/img/topic-queue-diagram.png create mode 100644 discv5/img/topic-radius-diagram.png diff --git a/discv5/discv5-rationale.md b/discv5/discv5-rationale.md index 95fad20..dcac08e 100644 --- a/discv5/discv5-rationale.md +++ b/discv5/discv5-rationale.md @@ -1,6 +1,6 @@ # Node Discovery Protocol v5 - Rationale -**Draft of August 2019** +**Draft of October 2019** Note that this specification is a work in progress and may change incompatibly without prior notice. @@ -342,24 +342,59 @@ disturb the operation of the protocol. Session keys per node-ID/IP generally pre replay across sessions. The `request-id`, mirrored in response packets, prevents replay of responses within a session. -## Security Considerations for the Topic Index +## The Topic Index -### Spamming with useless registrations +Using FINDNODE queries with appropriately chosen targets, the entire DHT can be sampled by +a random walk to find all other participants. When building a distributed application, it +is often desirable to restrict the search to participants which provide a certain service. +A simple solution to this problem would be to simply split up the network and require +participation in many smaller application-specific networks. However, such networks are +hard to bootstrap and also more vulnerable to attacks which could isolate nodes. + +The topic index provides discovery by provided service in a different way. Nodes maintain +a single node table tracking their neighbors and advertise 'topics' on nodes found by +randomly walking the DHT. While the 'global' topic index can be also spammed, it makes +complete isolation a lot harder. To prevent nodes interested in a certain topic from +finding each other, the entire discovery network would have to be overpowered. + +To make the index useful, searching for nodes by topic must be efficient regardless of the +number of advertisers. This is achieved by estimating the topic 'radius', i.e. the +percentage of all live nodes which are advertising the topic. Advertisement and search +activities are restricted to a region of DHT address space around the topic's 'center'. + +We also want the index to satisfy another property: When a topic advertisement is placed, +it should last for a well-defined amount of time. This ensures nodes may rely on their +advertisements staying placed rather than worrying about keeping them alive. + +Finally, the index should consume limited resources. Just as the node table is limited in +number and size of buckets, the size of the index data structure on each node is limited. + +### Why should advertisers wait? + +Advertisers must wait a certain amount of time before they can be registered. Enforcing +this time limit prevents misuse of the topic index because any topic must be important +enough to outweigh the cost of waiting. Imagine a group phone call: announcing the +participants of the call using topic advertisement isn't a good use of the system because +the topic exists only for a short time and will have very few participants. The waiting +time prevents using the index for this purpose because the call might already be over +before everyone could get registered. + +### Dealing with Topic Spam Our model is based on the following assumptions: -- Anyone can place their own advertisements under any topics and the rate of placing - registrations is not limited globally. The number of active registrations at any time is - roughly proportional to the resources (network bandwidth, mostly) spent on advertising. +- Anyone can place their own advertisements under any topics and the rate of placing ads + is not limited globally. The number of active ads for any node is roughly proportional + to the resources (network bandwidth, mostly) spent on advertising. - Honest actors whose purpose is to connect to other honest actors will spend an adequate - amount of efforts on registering and searching for registrations, depending on the rate - of newly established connections they are targeting. If the given topic is used only by - honest actors, a few registrations per minute will be satisfactory, regardless of the - size of the subnetwork. -- Dishonest actors may want to place an excessive amount of registrations just to disrupt - the discovery service. This will reduce the effectiveness of honest registration efforts - by increasing the topic radius and/or the waiting times. If the attacker(s) can place a - comparable amount or more registrations than all honest actors combined then the rate of + amount of efforts on registering and searching for ads, depending on the rate of newly + established connections they are targeting. If the given topic is used only by honest + actors, a few registrations per minute will be satisfactory, regardless of the size of + the subnetwork. +- Dishonest actors may want to place an excessive amount of ads just to disrupt the + discovery service. This will reduce the effectiveness of honest registration efforts by + increasing the topic radius and/or topic queue waiting times. If the attacker(s) can + place a comparable amount or more ads than all honest actors combined then the rate of new (useful) connections established throughout the network will reduce proportionally to the `honest / (dishonest + honest)` registration rates. @@ -371,33 +406,14 @@ honest actors is proportional to the square root of the attacker's efforts. ### Detecting a useless registration attack -In the case of a symmetrical protocol (where nodes are both searching and advertising -under the same topic) it is easy to detect when most of the queried registrations turn out -to be useless and increase both registration and query frequency. It is a bit harder but -still possible with asymmetrical (client-server) protocols, where only clients can easily -detect useless registrations, while advertisers (servers) do not have a direct way of -detecting when they should increase their advertising efforts. One possible solution is -for servers to also act as clients just to test the server capabilities of other -advertisers. It is also possible to implement a feedback system between trusted clients -and servers. - -### Amplifying network traffic by returning fake registrations - -An attacker might wish to direct discovery traffic to a chosen address by returning -records pointing to that address. - -**TBD: this is not solved.** - -### Not registering/returning valid registrations - -Although the limited registration frequency ensures that the resource requirements of -acting as a proper advertisement medium are sufficiently low, such selfish behavior is -possible, especially if some client implementations choose the easy way and not implement -it at all. This is not a serious problem as long as the majority of nodes are acting -properly, which will hopefully be the case. Advertisers can easily detect if their -registrations are not returned so it is probably possible to implement a mechanism to weed -out selfish nodes if necessary, but the design of such a mechanism is outside the scope of -this document. +In the case of a symmetrical protocol, where nodes are both searching and advertising +under the same topic, it is easy to detect when most of the found ads turn out to be +useless and increase both registration and query frequency. It is a bit harder but still +possible with asymmetrical (client-server) protocols, where only clients can easily detect +useless registrations, while advertisers (servers) do not have a direct way of detecting +when they should increase their advertising efforts. One possible solution is for servers +to also act as clients just to test the server capabilities of other advertisers. It is +also possible to implement a feedback system between trusted clients and servers. # References diff --git a/discv5/discv5-theory.md b/discv5/discv5-theory.md index 8af646f..4dad71f 100644 --- a/discv5/discv5-theory.md +++ b/discv5/discv5-theory.md @@ -1,6 +1,6 @@ # Node Discovery Protocol v5 - Theory -**Draft of August 2019.** +**Draft of October 2019.** Note that this specification is a work in progress and may change incompatibly without prior notice. @@ -26,7 +26,7 @@ used in place of the actual distance. logdistance(n₁, n₂) = log2(distance(n₁, n₂)) -## Maintaining The Local Record +## Maintaining The Local Node Record Participants should update their record, increase the sequence number and sign a new version of the record whenever their information changes. This is especially important for @@ -92,141 +92,199 @@ from the `k` closest nodes it has seen. ## Topic Advertisement -A node's provided services are identified by arbitrary strings called *topics*. Depending -on the needs of the application, a node can advertise multiple topics or no topics at all. -Every node participating in the discovery DHT acts as an advertisement medium, meaning -that it accepts topic registrations from advertising nodes and later returns them to nodes -searching for the same topic. - -The reason topic discovery is proposed in addition to application-specific networks is to -solve bootstrapping issues and improve downward scalability of subnetworks. Scalable -networks that have small subnetworks (and maybe even create new subnetworks automatically) -cannot afford to require a trusted bootnode for each of those subnets. Without a trusted -bootnode, small peer-to-peer networks are very hard to bootstrap and also more vulnerable -to attacks that could isolate nodes, especially the new ones which don't know any trusted -peers. Even though a global registry can also be spammed in order to make it harder to -find useful and honest peers, it makes complete isolation a lot harder because in order to -prevent the nodes of a small subnet from finding each other, the entire discovery network -would have to be overpowered. - -### Advertisement Storage - -Each node participating in the protocol stores ads for any number of topics and a limited -number of ads for each topic. The list of ads for a particular topic is called the *topic -queue* because it functions like a FIFO queue of limited length. There is also a global -limit on the number of ads regardless of the topic queue which contains them. When the -global limit is reached, the last entry of the least recently requested topic queue is -removed. - -For each topic queue, the advertisement medium maintains a *wait period*. This value acts -as a valve controlling the influx of new ads. Registrant nodes communicate interest to -register an ad and receive a *waiting ticket* which they can use to actually register -after the period has passed. Since regular communication among known nodes is required for -other purposes (e.g. node liveness checks), registrants re-learn the wait period values -automatically. - -The wait period for each queue is assigned based on the amount of sucessful registrations. -It is adjusted such that ads will stay in the topic queue for approximately 10 minutes. - -When an ad is added to the queue, the new wait period of the queue is computed as: - - target-ad-lifetime = 600 # how long ads stay queued (10 min) - target-registration-interval = target-ad-lifetime / queue-length - min-wait-period = 60 # (1 min) - control-loop-constant = 600 - - period = time-of-registration - time-of-previous-registration - new-wait-period = wait-period * exp((target-registration-interval - period) / control-loop-constant) - wait-period = max(new-wait-period, min-wait-period) +The topic advertisement subsystem indexes participants by their provided services. A +node's provided services are identified by arbitrary strings called 'topics'. A node +providing a certain service is said to 'place an ad' for itself when it makes itself +discoverable under that topic. Depending on the needs of the application, a node can +advertise multiple topics or no topics at all. Every node participating in the discovery +protocol acts as an advertisement medium, meaning that it accepts topic ads from other +nodes and later returns them to nodes searching for the same topic. + +### Topic Table + +Nodes store ads for any number of topics and a limited number of ads for each topic. The +data structure holding advertisements is called the 'topic table'. The list of ads for a +particular topic is called the 'topic queue' because it functions like a FIFO queue of +limited length. The image below depicts a topic table containing three queues. The queue +for topic `T₁` is at capacity. + +![topic table](./img/topic-queue-diagram.png) + +The queue size limit is implementation-defined. Implementations should place a global +limit on the number of ads in the topic table regardless of the topic queue which contains +them. Reasonable limits are 100 ads per queue and 50000 ads across all queues. Since ENRs +are at most 300 bytes in size, these limits ensure that a full topic table consumes +approximately 15MB of memory. + +Any node may appear at most once in any topic queue, that is, registration of a node which +is already registered for a given topic fails. Implementations may impose other +restrictions on the table, such as restrictions on the number of IP-addresses in a certain +range or number of occurrences of the same node across queues. + +### Tickets + +Ads should remain in the queue for a constant amount of time, the `target-ad-lifetime`. To +maintain this guarantee, new registrations are throttled and registrants must wait for a +certain amount of time before they are admitted. When a node attempts to place an ad, it +receives a 'ticket' which tells them how long they must wait before they will be accepted. +It is up to the registrant node to keep the ticket and present it to the advertisement +medium when the waiting time has elapsed. + +The waiting time constant is: + + target-ad-lifetime = 15min + +The assigned waiting time for any registration attempt is determined according to the +following rules: + +- When the table is full, the waiting time is assigned based on the lifetime of the oldest + ad across the whole table, i.e. the registrant must wait for a table slot to become + available. +- When the topic queue is full, the waiting time depends on the lifetime of the oldest ad + in the queue. The assigned time is `target-ad-lifetime - oldest-ad-lifetime` in this + case. +- Otherwise the ad may be placed immediately. + +Tickets are opaque objects storing arbitrary information determined by the issuing node. +While details of encoding and ticket validation are up to the implementation, tickets must +contain enough information to verify that: + +- The node attempting to use the ticket is the node which requested it. +- The ticket is valid for a single topic only. +- The ticket can only be used within the registration window. +- The ticket can't be used more than once. + +Implementations may choose to include arbitrary other information in the ticket, such as +the cumulative wait time spent by the advertiser. A practical way to handle tickets is to +encrypt and authenticate them with a dedicated secret key: + + ticket = aesgcm_encrypt(ticket-key, ticket-nonce, ticket-pt, '') + ticket-pt = [src-node-id, src-ip, topic, req-time, wait-time, cum-wait-time] + src-node-id = node ID that requested the ticket + src-ip = IP address that requested the ticket + topic = the topic that ticket is valid for + req-time = absolute time of REGTOPIC request + wait-time = waiting time assigned when ticket was created + cum-wait = cumulative waiting time of this node + +### Registration Window + +The image below depicts a single ticket's validity over time. When the ticket is issued, +the node keeping it must wait until the registration window opens. The length of the +registration window is 10 seconds. The ticket becomes invalid after the registration +window has passed. + +![ticket validity over time](./img/ticket-validity.png) + +Since all ticket waiting times are assigned to expire when a slot in the queue opens, the +advertisement medium may receive multiple valid tickets during the registration window and +must choose one of them to be admitted in the topic queue. The winning node is notified +using a [REGCONFIRMATION] response. + +Picking the winner can be achieved by keeping track of a single 'next ticket' per queue +during the registration window. Whenever a new ticket is submitted, first determine its +validity and compare it against the current 'next ticket' to determine which of the two is +better according to an implementation-defined metric such as the cumulative wait time +stored in the ticket. ### Advertisement Protocol -Let us assume that node `A` advertises itself under topic `T`. It selects node `C` as -advertisement medium and wants to register an ad, so that when node `B` (who is searching -for topic `T`) asks `C`, `C` can return the registration entry of `A` to `B`. +This section explains how the topic-related protocol messages are used to place an ad. -Node `A` first tells `C` that it wishes to register by requesting a ticket for topic `T`, -using the [REQTICKET] message. +Let us assume that node `A` provides topic `T`. It selects node `C` as advertisement +medium and wants to register an ad, so that when node `B` (who is searching for topic `T`) +asks `C`, `C` can return the registration entry of `A` to `B`. - A -> C REQTICKET +Node `A` first attempts to register without a ticket by sending [REGTOPIC] to `C`. -`C` replies with a ticket. The ticket contains the node identifier of `A`, the topic, a -serial number and wait period assigned by `C`. + A -> C REGTOPIC [T, ""] - A <- C TICKET +`C` replies with a ticket and waiting time. -Node `A` now waits for the duration of the wait period. When the wait is over, `A` sends a -registration request including the ticket. `C` does not need to remember its issued -tickets, just the serial number of the latest ticket accepted from `A` (after which it -will not accept any tickets issued earlier). + A <- C TICKET [ticket, wait-time] - A -> C REGTOPIC +Node `A` now waits for the duration of the waiting time. When the wait is over, `A` sends +another registration request including the ticket. `C` does not need to remember its +issued tickets since the ticket is authenticated and contains enough information for `C` +to determine its validity. -If the ticket was valid, Node `C` places `A` into the topic queue for `T`. The -[REGCONFIRMATION] response message signals whether `A` is registered. + A -> C REGTOPIC [T, ticket] - A <- C REGCONFIRMATION +Node `C` replies with another ticket. Node `A` must keep this ticket in place of the +earlier one, and must also be prepared to handle a confirmation call in case registration +was successful. -### Ad Placement And Topic Radius Detection + A <- C TICKET [ticket, wait-time] -When the number of nodes advertising a topic (topic size) is at least a certain percentage -of the whole discovery network (rough estimate: at least 1%), it is sufficient to select -random nodes to place ads and also look for ads at randomly selected nodes. In case of a -very high network size/topic size ratio, it helps to have a convention for selecting a -subset of nodes as potential advertisement media. This subset is defined as the nodes -whose Kademlia address is close to `keccak256(T)`, meaning that the binary XOR of the -address and the topic hash interpreted as a fixed point number is smaller than a given -*topic radius*. A radius of 1 means the entire network, in which case advertisements are -distributed uniformly. +Node `C` waits for the registration window to end on the queue and selects `A` as the node +which is registered. Node `C` places `A` into the topic queue for `T` and sends a +[REGCONFIRMATION] response. -Example: + A <- C REGCONFIRMATION [T] -- Nodes in the topic discovery network: 10000 -- Number of advertisers of topic T: 100 -- Registration frequency: 3 per minute -- Average registration lifetime: 10 minutes -- Average number of registrations of topic T at any moment: `3 * 10 * 100 = 3000` -- Expected number of registrations of T found at a randomly selected node (topic density) - assuming a topic radius of 1: 0.3 +### Ad Placement And Topic Radius -When the number of advertisers is smaller than 1% of the entire network, we want to -decrease the topic radius proportionally in order to keep the topic density at a -sufficiently high level. To achieve this, both advertisers and searchers should initially -try selecting nodes with an assumed topic radius of 1 and collect statistical data about -the density of registrations at the selected nodes. If the topic density in the currently -assumed topic radius is under the target level (0.3 in our example), the radius is -decreased. There is no point in decreasing the targeted node subset under the size of -approximately 100 nodes since in this case even a single advertiser can easily be found. -Approximating the density of nodes in a given address space is possible by calculating the -average distance between a randomly selected address and the address of the closest actual -node found. If the approximated number of nodes in our topic radius is under 100, we -increase the radius. +Since every node may act as an advertisement medium for any topic, advertisers and nodes +looking for ads must agree on a scheme by which ads for a topic are distributed. When the +number of nodes advertising a topic is at least a certain percentage of the whole +discovery network (rough estimate: at least 1%), ads may simply be placed on random nodes +because searching for the topic on randomly selected will locate the ads quickly enough. + +However, topic search should be fast even when the number of advertisers for a topic is +much smaller than the number of all live nodes. Advertisers and searchers must agree on a +subset of nodes to serve as advertisement media for the topic. This subset is simply a +region of node ID address space, consisting of nodes whose Kademlia address is within a +certain distance to the topic hash `sha256(T)`. This distance is called the 'topic +radius'. + +Example: for a topic `f3b2529e...` with a radius of 2^240, the subset covers all nodes +whose IDs have prefix `f3b2...`. A radius of 2^256 means the entire network, in which case +advertisements are distributed uniformly among all nodes. The diagram below depicts a +region of address space with the topic hash `t` in the middle and several nodes close to +`t` surrounding it. Dots above the nodes represent entries in the node's queue for the +topic. + +![diagram explaining the topic radius concept](./img/topic-radius-diagram.png) + +To place their ads, participants simply perform a random walk within the currently +estimated radius and run the advertisement protocol by collecting tickets from all nodes +encountered during the walk and using them when their waiting time is over. + +### Topic Radius Estimation + +Advertisers must estimate the topic radius continuously in order to place their ads on +nodes where they will be found. The radius mustn't fall below a certain size because +restricting registration to too few nodes leaves the topic vulnerable to censorship and +leads to long waiting times. If the radius were too large, searching nodes would take too +long to find the ads. + +Estimating the radius uses the waiting time as an indicator of how many other nodes are +attempting to place ads in a certain region. This is achieved by keeping track of the +average time to successful registration within segments of the address space surrounding +the topic hash. Advertisers initially assume the radius is 2^256, i.e. the entire network. +As tickets are collected, the advertiser samples the time it takes to place an ad in each +segment and adjusts the radius such that registration at the chosen distance takes +approximately `target-ad-lifetime / 2` to complete. ## Topic Search Finding nodes that provide a certain topic is a continuous process which reads the content -of topic queues inside the approximated topic radius. Nodes within the radius are -contacted with [TOPICQUERY] packets. Collecting tickets and waiting on them is not -required. The approximated topic radius value can be shared with the registration -algorithm if the the same topic is being registered and searched for. - -To find nodes, the searcher generates random node IDs inside the topic radius and performs -recursive Kademlia lookups on them. All (intermediate) nodes encountered during lookup are -asked for topic queue enties using the [TOPICQUERY] packet. - -Topic search is not meant to be the only mechanism used for selecting peers. A persistent -database of useful peers is also recommended, where the meaning of "useful" is -protocol-specific. Like any DHT algorithm, topic advertisement is based on the law of -large numbers. It is easy to spread junk in it at the cost of wasting some resources. -Creating a more trusted sub-network of peers over time prevents any such attack from -disrupting operation, removing incentives to waste resources on trying to do so. A -protocol-level recommendation-based trust system can be useful, the protocol may even have -its own network topology. +of topic queues inside the approximated topic radius. This is a much simpler process than +topic advertisement because collecting tickets and waiting on them is not required. + +To find nodes for a topic, the searcher generates random node IDs inside the estimated +topic radius and performs Kademlia lookups for these IDs. All (intermediate) nodes +encountered during lookup are asked for topic queue entries using the [TOPICQUERY] packet. + +Radius estimation for topic search is similar to the estimation procedure for +advertisement, but samples the average number of results from TOPICQUERY instead of +average time to registration. The radius estimation value can be shared with the +registration algorithm if the the same topic is being registered and searched for. [EIP-778]: https://eips.ethereum.org/EIPS/eip-778 [PING]: ./discv5-wire.md#ping-request-0x01 [PONG]: ./discv5-wire.md#pong-response-0x02 [FINDNODE]: ./discv5-wire.md#findnode-request-0x03 -[REQTICKET]: ./discv5-wire.md#reqticket-request-0x05 -[REGCONFIRMATION]: ./discv5-wire.md#regconfirmation-response-0x08 -[TOPICQUERY]: ./discv5-wire.md#topicquery-request-0x09 +[REGTOPIC]: ./discv5-wire.md#regtopic-request-0x05 +[REGCONFIRMATION]: ./discv5-wire.md#regconfirmation-response-0x07 +[TOPICQUERY]: ./discv5-wire.md#topicquery-request-0x08 diff --git a/discv5/discv5-wire.md b/discv5/discv5-wire.md index f4b8cd5..8f4eb76 100644 --- a/discv5/discv5-wire.md +++ b/discv5/discv5-wire.md @@ -1,6 +1,6 @@ # Node Discovery Protocol v5 - Wire Protocol -**Draft of August 2019.** +**Draft of October 2019.** This document specifies the wire protocol of Node Discovery v5. Note that this specification is a work in progress and may change incompatibly without prior notice. @@ -273,17 +273,21 @@ current record as the only result. NODES is the response to a FINDNODE or TOPICQUERY message. Multiple NODES messages may be sent as responses to a single query. -### REQTICKET Request (0x05) +### REGTOPIC Request (0x05) - message-data = [request-id, topic] - message-type = 0x05 - topic = a 32-byte topic hash + message-data = [request-id, ENR, ticket] + message-type = 0x07 + node-record = current node record of sender + ticket = byte array containing ticket content -Implementation note: The least requested topics will be evicted from the global space. -This means that an attacker attempting to pollute the global space by requesting creation -of many *new* topic queues will only result in their own topic queues being evicted. -Implementers should be cautious of the attacker attempting to promote their own queues by -requesting their own adverts. +REGTOPIC attempts to register the sender for the given topic. If the requesting node has a +ticket from a previous registration attempt, it must present the ticket. Otherwise +`ticket` is the empty byte array (RLP: `0x80`). The ticket must be valid and its waiting +time must have elapsed before using the ticket. + +REGTOPIC is always answered by a TICKET response. The requesting node may also receive a +REGCONFIRMATION response when registration is successful. It may take up to 10s for the +confirmation to be sent. ### TICKET Response (0x06) @@ -292,49 +296,32 @@ requesting their own adverts. ticket = an opaque byte array representing the ticket wait-time = time to wait before registering, in seconds -TICKET is the response to REQTICKET. It contains a ticket which can be used to register -for the requested topic after `wait-time` has elapsed. - -Note that `ticket` is opaque for the caller and shouldn't be interpreted in any way. -Implementations may choose any internal representation. A practical way to handle tickets -is to encrypt and authenticate them with a separate key. - - ticket = aesgcm_encrypt(ticket-key, ticket-nonce, ticket-pt, '') - ticket-pt = [src-node-id, topic, req-time, wait-time, serial] - src-node-id = node ID that requested the ticket - topic = the topic that ticket is valid for - req-time = absolute time of REQTICKET request - wait-time = waiting time assigned when ticket was created - serial = serial number of ticket +TICKET is the response to REGTOPIC. It contains a ticket which can be used to register for +the requested topic after `wait-time` has elapsed. See the [theory section on tickets] for +more information. -### REGTOPIC Request (0x07) +### REGCONFIRMATION Response (0x07) - message-data = [request-id, ticket, ENR] - message-type = 0x07 - ticket = supplied by TICKET response - node-record = current node record of sender - -REGTOPIC registers the sender for the given topic with a ticket. The ticket must be valid -and its waiting time must have elapsed before using the ticket. - -### REGCONFIRMATION Response (0x08) - - message-data = [request-id, registered] + message-data = [request-id, topic] message-type = 0x07 - registered = boolean, 1 if ticket was valid and node is registered, 0 if not + request-id = request-id of REGTOPIC -REGCONFIRMATION is the response to REGTOPIC. +REGCONFIRMATION notifies the recipient about a successful registration for the given +topic. This call is sent by the advertisement medium after the time window for +registration has elapsed on a topic queue. -### TOPICQUERY Request (0x09) +### TOPICQUERY Request (0x08) message-data = [request-id, topic] message-type = 0x07 topic = 32-byte topic hash -TOPICQUERY requests nodes in the [topic queue] of the given topic. The response is a NODES -message containing node records registered for the topic. +TOPICQUERY requests nodes in the [topic queue] of the given topic. The recipient of this +request must send one or more NODES messages containing node records registered for the +topic. [handshake section]: #handshake [encoding section]: #packet-encoding -[topic queue]: ./discv5-theory.md#advertisement-storage +[topic queue]: ./discv5-theory.md#topic-table +[theory section on tickets]: ./discv5-theory.md#tickets [EIP-778]: https://eips.ethereum.org/EIPS/eip-778 diff --git a/discv5/discv5.md b/discv5/discv5.md index bc53028..619012e 100644 --- a/discv5/discv5.md +++ b/discv5/discv5.md @@ -1,6 +1,6 @@ # Node Discovery Protocol v5 -**Draft of April 2019.** +**Draft of October 2019.** Welcome to the Node Discovery Protocol v5 specification! diff --git a/discv5/img/ticket-validity.png b/discv5/img/ticket-validity.png new file mode 100644 index 0000000000000000000000000000000000000000..4e32a8f684e17147beac840829ea1556297bfc44 GIT binary patch literal 13800 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 OkunGc_?M?I-uw?AJ95zg literal 0 HcmV?d00001 diff --git a/discv5/img/topic-queue-diagram.png b/discv5/img/topic-queue-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..e34a1c58e77e196d573a1942c6629042a54f6846 GIT binary patch literal 5816 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 QfKm`ec@?=lnY$1E59OPW&;S4c literal 0 HcmV?d00001