Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds new contact-info with forward compatible sockets #29596

Merged
merged 1 commit into from
Jan 26, 2023

Conversation

behzadnouri
Copy link
Contributor

@behzadnouri behzadnouri commented Jan 9, 2023

Problem

  • Need to add/remove sockets from ContactInfo in a backward and forward compatible way.
  • Current sockets specification in LegacyContactInfo is inefficient and unnecessarily duplicates IP addresses.

Summary of Changes

Implemented a new ContactInfo where

  • Ports and IP addresses are specified separately so that unique IP addresses can only be specified once.
  • Different sockets (tvu, tpu, etc) are specified by opaque u8 tags so that adding and removing sockets is backward and forward compatible.
  • solana_version::Version is also embedded in so that it won't need to be gossiped separately.
  • In order to identify duplicate running instances, the struct also includes when the instance was first created and so the separate NodeInstance crds-value will become redundant.

Update plan:

  • Once the cluster is able to ingest the new type (i.e. this patch), a 2nd patch will start gossiping the new ContactInfo along with the LegacyContactInfo.
  • Once all nodes in the cluster gossip the new ContactInfo, a 3rd patch will start solely using the new ContactInfo while still gossiping the old LegacyContactInfo.
  • Once all nodes in the cluster solely use the new ContactInfo, a 4th patch will stop gossiping the old LegacyContactInfo.

@ryoqun
Copy link
Member

ryoqun commented Jan 11, 2023

@behzadnouri i pushed commits to fix the frozen abi issue with Cow: 28a0c38 and 552d13d

ref: rust-lang/rust#106710

@behzadnouri
Copy link
Contributor Author

@behzadnouri i pushed commits to fix the frozen abi issue with Cow: 28a0c38 and 552d13d

ref: rust-lang/rust#106710

@ryoqun Thank you

gossip/src/contact_info.rs Show resolved Hide resolved
gossip/src/contact_info.rs Show resolved Hide resolved
Comment on lines 22 to 31
const SOCKET_TAG_GOSSIP: u8 = 0;
const SOCKET_TAG_REPAIR: u8 = 1;
const SOCKET_TAG_RPC: u8 = 2;
const SOCKET_TAG_RPC_PUBSUB: u8 = 3;
const SOCKET_TAG_SERVE_REPAIR: u8 = 4;
const SOCKET_TAG_TPU: u8 = 5;
const SOCKET_TAG_TPU_QUIC: u8 = 6;
const SOCKET_TAG_TPU_FORWARDS: u8 = 7;
const SOCKET_TAG_TPU_FORWARDS_QUIC: u8 = 8;
const SOCKET_TAG_TPU_VOTE: u8 = 9;
const SOCKET_TAG_TVU: u8 = 10;
const SOCKET_TAG_TVU_FORWARDS: u8 = 11;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider an enum or type-alias/newtype u8 here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opaque u8 here is intentional so that adding new sockets is backward compatible.

An enum will fail deserialization if there is an unknown tag/enum-variant; as a result you cannot add a new socket in a backward compatible way because nodes not recognizing the enum-variant would fail deserializing the whole contact-info.

You can work around that by adding an Unknown(u8) variant to the enum but that would also require manual serde implementation which would be counterproductive.

gossip/src/contact_info.rs Show resolved Hide resolved
Comment on lines 199 to 202
// TODO: sanity check on the fields.
// TODO: verify port offsets don't overflow.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these seem important?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Comment on lines +228 to +422
fn new_rand_addr<R: Rng>(rng: &mut R) -> IpAddr {
if rng.gen() {
let addr = Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen());
IpAddr::V4(addr)
} else {
let addr = Ipv6Addr::new(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
);
IpAddr::V6(addr)
}
}

fn new_rand_port<R: Rng>(rng: &mut R) -> u16 {
let port = rng.gen::<u16>();
let bits = u16::BITS - port.leading_zeros();
let shift = rng.gen_range(0u32, bits + 1u32);
port.checked_shr(shift).unwrap_or_default()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these will occasionally draw values that we'd reject on sanitize, right? probably less flaky to generate the deterministically ala Pubkey::new_unique

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what change you are suggesting.
This occasionally creates invalid socket addresses which is intentional to have test coverage over paths which fail sanitize_socket.

If we want to make the test deterministic we can use a SeedableRng and a seeded random number generator.
Pubkey::new_unique approach isn't good because:
https://discord.com/channels/428295358100013066/439194979856809985/881943239823880222

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what change you are suggesting. This occasionally creates invalid socket addresses which is intentional to have test coverage over paths which fail sanitize_socket.

we shouldn't mix testing and fuzzing like that. tests should be deterministic to avoid flake. if we need to start fuzzing stuff, that's another conversation

If we want to make the test deterministic we can use a SeedableRng and a seeded random number generator. Pubkey::new_unique approach isn't good because: https://discord.com/channels/428295358100013066/439194979856809985/881943239823880222

seedable rng would be fine (would be fine to reimplement Pubkey::new_unique over one as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if all corner cases are identified, it is impossible to cover all their various combinations using deterministic tests. Using unseeded random number generator here, we are getting a much larger test coverage.

Flaky tests by themselves are not a problem, as long as they are easily reproducible locally; in which case they do identify a legit bug or corner case.

Given that this is a single threaded test and there is no other dependence on runtime environment here, do you see any reason that if this test becomes flaky it won't be locally reproducible, by say running the test 1000 times in a loop?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about switch to deterministic rng, seed with time and log the seed? so we can immediately reproduce a failure with trivial modification

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you didn't answer my question

gossip/src/contact_info.rs Outdated Show resolved Hide resolved
let &(_, index, port) = self.sockets.iter().find(|(k, _, _)| *k == tag)?;
let addr = self.addrs.get(usize::from(index))?;
let socket = SocketAddr::new(*addr, port);
let _ = sanitize_socket(&socket).ok()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this should've been done on construction/push?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_socket does sanitize_socket.
But I am thinking if we are deserializing another node's contact-info, we probably don't want to throw the entire contact-info out if one of the sockets doesn't sanitize. It would be just enough to filter out that specific socket when needed.

gossip/src/contact_info.rs Outdated Show resolved Hide resolved
thiserror::Error,
};

const SOCKET_TAG_GOSSIP: u8 = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a gossip quic entry as well? We want to add Quic Gossip support. CC @lijunwangs on the related Quic repair changes for if we need a port allocated for that as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding unused ports now, we can add new ports once the code using those ports is almost ready (could possibly be in the same patch). Even if we add that now it cannot be used yet until LegacyContactInfo => ContactInfo migration is done.

#[serde(with = "short_vec")]
addrs: Cow<'a, [IpAddr]>,
#[serde(with = "short_vec")]
sockets: Cow<'a, [(u8, u8, u16)]>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can define a struct to clarify what these three members means. Would this make the sockets dynamic to add additional sockets? Like quic tpu and etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

fn get_socket(&self, tag: u8) -> Option<SocketAddr> {
let &(_, index, port) = self.sockets.iter().find(|(k, _, _)| *k == tag)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put it into a map for quicker lookup?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the list will be short. probably faster to linear probe than do the hashing for a map

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual socket lookup is done by self.cache which is an array.

pub struct ContactInfo {
    // ...
    #[serde(skip_serializing)]
    cache: [SocketAddr; SOCKET_CACHE_SIZE],
}

gossip/src/contact_info.rs Outdated Show resolved Hide resolved
@behzadnouri
Copy link
Contributor Author

@lijunwangs @t-nelson
This is not ready for code review; It is marked as "Draft" and you see things like #![allow(dead_code)], TODO etc.

The intention of discord message was just to discuss the new ContactInfo struct design-wise:
https://discord.com/channels/428295358100013066/439194979856809985/1062804586118795315

@mvines
Copy link
Member

mvines commented Jan 12, 2023

Should we roll NodeInstance into this as well? Really that's separate data item only because we couldn't extend ContactInfo:

pub struct NodeInstance {
from: Pubkey,
wallclock: u64,
timestamp: u64, // Timestamp when the instance was created.
token: u64, // Randomly generated value at node instantiation.
}

@behzadnouri
Copy link
Contributor Author

Should we roll NodeInstance into this as well? Really that's separate data item only because we couldn't extend ContactInfo:

pub struct NodeInstance {
from: Pubkey,
wallclock: u64,
timestamp: u64, // Timestamp when the instance was created.
token: u64, // Randomly generated value at node instantiation.
}

yup, doing that as well.

@behzadnouri behzadnouri force-pushed the contact-info-packed branch 2 times, most recently from d86d437 to 6b690c7 Compare January 18, 2023 15:05
@behzadnouri behzadnouri marked this pull request as ready for review January 18, 2023 15:39
@behzadnouri behzadnouri force-pushed the contact-info-packed branch 2 times, most recently from 754599e to 8102031 Compare January 18, 2023 16:23
gossip/src/crds_gossip_pull.rs Show resolved Hide resolved
@@ -189,6 +189,10 @@ impl CrdsGossipPush {
let crds = crds.read().unwrap();
let entries = crds
.get_entries(crds_cursor.deref_mut())
.filter(|entry| {
// Exclude the new ContactInfo until the cluster has upgraded.
!matches!(&entry.value.data, CrdsData::ContactInfo(_))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Contributor Author

@behzadnouri behzadnouri Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gossip/src/contact_info.rs Show resolved Hide resolved
gossip/src/contact_info.rs Show resolved Hide resolved
gossip/src/contact_info.rs Show resolved Hide resolved
gossip/src/contact_info.rs Outdated Show resolved Hide resolved
gossip/src/contact_info.rs Outdated Show resolved Hide resolved
}
}
}
// Verify that all sockets have unique key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate this section in the comment? i understand what's going on here, but not how it relates to the rest of this changeset

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key should be unique otherwise with multiple entries for the same key, the socket specification would be ambiguous as which one it is. This is already commented in the struct definition:

pub struct ContactInfo {
    // ...
    // All sockets have a unique key and a valid IP address index.
    #[serde(with = "short_vec")]
    sockets: Vec<SocketEntry>,
    // ...
}

here I am verifying "that all sockets have unique key".

What needs to be clarified?

Comment on lines +228 to +422
fn new_rand_addr<R: Rng>(rng: &mut R) -> IpAddr {
if rng.gen() {
let addr = Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen());
IpAddr::V4(addr)
} else {
let addr = Ipv6Addr::new(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
);
IpAddr::V6(addr)
}
}

fn new_rand_port<R: Rng>(rng: &mut R) -> u16 {
let port = rng.gen::<u16>();
let bits = u16::BITS - port.leading_zeros();
let shift = rng.gen_range(0u32, bits + 1u32);
port.checked_shr(shift).unwrap_or_default()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what change you are suggesting. This occasionally creates invalid socket addresses which is intentional to have test coverage over paths which fail sanitize_socket.

we shouldn't mix testing and fuzzing like that. tests should be deterministic to avoid flake. if we need to start fuzzing stuff, that's another conversation

If we want to make the test deterministic we can use a SeedableRng and a seeded random number generator. Pubkey::new_unique approach isn't good because: https://discord.com/channels/428295358100013066/439194979856809985/881943239823880222

seedable rng would be fine (would be fine to reimplement Pubkey::new_unique over one as well)


#[derive(Copy, Clone, Debug, Eq, PartialEq, AbiExample, Deserialize, Serialize)]
struct SocketEntry {
key: u8, // Protocol identifier, e.g. tvu, tpu, etc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we normalize on key or tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is key everywhere. There is no tag.
..._TAG_... is only used for constants (because those are consts not a key variable).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, but we assign a *_TAG_* constant to a key field. which i find inconsistent

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took me a while to realize the "key" in this context is the one of the tags defined as well. I think it is clearer to standardize on one only

@behzadnouri
Copy link
Contributor Author

@t-nelson were you going to look at this again or are you ok to merge it?

The commit implement new ContactInfo where
* Ports and IP addresses are specified separately so that unique IP
  addresses can only be specified once.
* Different sockets (tvu, tpu, etc) are specified by opaque u8 tags so
  that adding and removing sockets is backward and forward compatible.
* solana_version::Version is also embedded in so that it won't need to
  be gossiped separately.
* NodeInstance is also rolled in by adding a field identifying when the
  instance was first created so that it won't need to be gossiped
  separately.

Update plan:
* Once the cluster is able to ingest the new type (i.e. this patch), a
  2nd patch will start gossiping the new ContactInfo along with the
  LegacyContactInfo.
* Once all nodes in the cluster gossip the new ContactInfo, a 3rd patch
  will start solely using the new ContactInfo while still gossiping the
  old LegacyContactInfo.
* Once all nodes in the cluster solely use the new ContactInfo, a 4th
  patch will stop gossiping the old LegacyContactInfo.
@t-nelson
Copy link
Contributor

yes. i'll take another pass today


#[derive(Copy, Clone, Debug, Eq, PartialEq, AbiExample, Deserialize, Serialize)]
struct SocketEntry {
key: u8, // Protocol identifier, e.g. tvu, tpu, etc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took me a while to realize the "key" in this context is the one of the tags defined as well. I think it is clearer to standardize on one only

key: u8, // Protocol identifier, e.g. tvu, tpu, etc
index: u8, // IpAddr index in the accompanying addrs vector.
#[serde(with = "serde_varint")]
offset: u16, // Port offset with respect to the previous entry.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the offset is not totally accurate, for the first entry it is the actual port -- so better to reflect it in the comment at least. I wonder what is the benefit of storing the offset than simply the port number itself as most complexity in this code is related to the offset arithmetic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ports tend to be close to each other, so the offsets can be stored in one byte.
For gossip we are consuming a lot of bandwidth so anything which helps in the front can be useful.
Any added complexity is encapsulated inside this struct, and this is not exposed in the public api.

@behzadnouri behzadnouri merged commit d6fbf3f into solana-labs:master Jan 26, 2023
@behzadnouri behzadnouri deleted the contact-info-packed branch January 26, 2023 17:02
behzadnouri added a commit to behzadnouri/solana that referenced this pull request Apr 21, 2023
New ContactInfo implemented in
solana-labs#29596
specifies sockets for QUIC protocols explicitly.
The commit adds tpu_quic field to RpcContactInfo in preparation of
LegacyContactInfo => ContactInfo migration.
behzadnouri added a commit to behzadnouri/solana that referenced this pull request Apr 21, 2023
New ContactInfo implemented in
solana-labs#29596
specifies sockets for QUIC protocols explicitly.
The commit adds tpu_quic field to RpcContactInfo in preparation of
LegacyContactInfo => ContactInfo migration.
behzadnouri added a commit to behzadnouri/solana that referenced this pull request Nov 15, 2023
Working towards migrating from legacy contact-info to the new
contact-info:
solana-labs#29596
behzadnouri added a commit that referenced this pull request Nov 15, 2023
Working towards migrating from legacy contact-info to the new
contact-info:
#29596
mergify bot pushed a commit that referenced this pull request Nov 15, 2023
Working towards migrating from legacy contact-info to the new
contact-info:
#29596

(cherry picked from commit ba0a49b)
mergify bot added a commit that referenced this pull request Nov 15, 2023
…4092) (#34095)

propagates the new contact-info through gossip (#34092)

Working towards migrating from legacy contact-info to the new
contact-info:
#29596

(cherry picked from commit ba0a49b)

Co-authored-by: behzad nouri <[email protected]>
crossdev24 pushed a commit to crossdev24/solana that referenced this pull request Jun 24, 2024
Working towards migrating from legacy contact-info to the new
contact-info:
solana-labs/solana#29596
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants