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

docs: draft HardForkCombinatorRollbackMonotonicityProposal.md #346

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

nfrisby
Copy link
Contributor

@nfrisby nfrisby commented Sep 15, 2023

This an attempt at the first step of Issue #389.

@nfrisby
Copy link
Contributor Author

nfrisby commented Sep 15, 2023

@nfrisby nfrisby force-pushed the nfrisby/hfc-proposal branch 8 times, most recently from 1fe5f75 to c89615d Compare September 15, 2023 00:29
- Let R count the number of blocks after L on D.
Observe that there must be more than R blocks after L on C, since C is better than D.
(This isn't necessarily true, due to tiebreakers.
However, even with tiebreakers, this remains true in the worst case, which is what ultimately matters.)
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me what the worst case is here, and why this is true in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this parenthetical is admittedly handwaivy.

C can be better than D in two ways: it can be the same length and have a better tiebreaker or it can be longer. When it's longer requires a deeper rollback from us. That extra depth is the "worst case" aspect here.

However, even with tiebreakers, this remains true in the worst case, which is what ultimately matters.)
- Common Prefix ensures R <= k.
- L must contain enough information for the node to correctly judge the validity of the k+1th block after L on C.
- Otherwise the Header-Body Split would prevent the node from adopting C, even though doing so requires at most rolling back k blocks.
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the "Otherwise" bullet can be merged with the previous one, or indented one level.


Note that, if necessary, L can be ticked in order to validate the first header after L on C, without requiring any predictions.
In the absence of the corresponding block body---ie because of the Header-Body Split---the assessment of subsequent headers on C will require predictions.
Thus a prediction range of at least 3k/f ensures at least k blocks after the first header, and hence k+1 blocks in total, as necessary.
Copy link
Member

Choose a reason for hiding this comment

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

Why k+1 in total? Are we counting the header of the block that led to L?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suppose Y is the first block after L on C. The forecasting allows for k blocks after Y on C. So that's k+1 total.

- L must contain enough information for the node to correctly judge the validity of the k+1th block after L on C.
- Otherwise the Header-Body Split would prevent the node from adopting C, even though doing so requires at most rolling back k blocks.

That is a complete answer.
Copy link
Member

Choose a reason for hiding this comment

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

What constitutes a complete answer in this context? (and how do we convince ourselves this is one of them?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What constitutes a complete answer in this context?

The next sentence was intended to clarify that.

how do we convince ourselves this is one of them?

Conversation, property testing, etc :D

If the safe zone were instead less than one stability window, then L does not necessarily contain enough information to validate the R+1th header after L on C, _even without_ a Chain Growth violation.
Specifically, the node will need to know if the chain will actually be in whichever era that header claims to be in, and it wouldn't necessarily be able to answer that question more than a safe zone after the first header after L on C.

The answer to Question 2 does change: there is one additional change in behavior possibly enabled by a Chain Growth violation.
Copy link
Member

Choose a reason for hiding this comment

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

I think a concrete example would be great here.

GetDRepStakeDistr :: Set (LC.DRep (EraCrypto era)) -> BlockQuery (ShelleyBlock proto era) (Map (LC.DRep (EraCrypto era)) Coin)
GetCommitteeState :: BlockQuery (ShelleyBlock proto era) (SL.CommitteeState era)

The queries marked with `*` and/or `+` are the only queries whose responses exhibit weak monotinicity as the node improves its selection by switching/extending chains.
Copy link
Member

Choose a reason for hiding this comment

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

Could we define, explain and exemplify "weak monotonicity" in this context?

Copy link
Contributor Author

@nfrisby nfrisby Sep 21, 2023

Choose a reason for hiding this comment

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

It's a slightly more complicated general version of "non-descending". I think it'd actually be a non-trivial aside. But I have no doubt a reference exists.

@nfrisby
Copy link
Contributor Author

nfrisby commented Sep 25, 2023

Just a point of reference: the node CLI query for the leader schedule is perhaps also in the same gray area as time translations. Should that query refuse to answer until a rollback by <= k blocks could not change it?

Edit: it was discussed, and yes it should refuse. See Issue #390.

dnadales
dnadales previously approved these changes Oct 3, 2023
@dnadales dnadales dismissed their stale review October 3, 2023 12:53

Nick would like me to fully understand the document before approving

@dnadales dnadales self-assigned this Oct 3, 2023
@nfrisby
Copy link
Contributor Author

nfrisby commented Oct 3, 2023

I pushed up an Introduction; hopefully that makes the document easier to approach.

@nfrisby
Copy link
Contributor Author

nfrisby commented Oct 3, 2023

I also pushed up some adjustments for the smaller comments you made.


| DST | CCT | PIL | DSR | ZEE | ZBE | QSR |
| --- | --- | --- | --- | --- | --- | --- |
| I | A | P 1 | I | 2 | 3 | I |
Copy link
Member

Choose a reason for hiding this comment

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

About DST: I: I would have expected that it is sensitive to ticks since ticking might cause votes to be counted and a hardfork could be decided as a consequence of this. Would it be possible to explain why this is not the case?

Copy link
Contributor Author

@nfrisby nfrisby Oct 11, 2023

Choose a reason for hiding this comment

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

Yeah. Extremely high-level summary: it's because of PIL=Possible. The ticking itself does make the vote-counting actually happen, but if there hasn't been enough blocks, the Cardano HFC logic will "veto" the decision to end the era and simply not transition to the next era.

Copy link
Member

Choose a reason for hiding this comment

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

The ticking itself does make the vote-counting actually happen

Oh, I thought this was the case. Do you know how we currently determine if we should transition to the next era based on the ledger info? I guess that we do not rely on this rule on epoch change. Do we roll out our own logic (that totally ignores the ledger?).

image

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 ticking itself does make the vote-counting actually happen

Oh, I thought this was the case.

I sounds like maybe you read "doesn't" because I wrote "does", which is a bit unusual.

Do we roll out our own logic (that totally ignores the ledger?)

Here's the entrypoint to the Consensus-side logic https://github.com/input-output-hk/ouroboros-consensus/blob/51da3876c01edc2eec250fdc998f6cb33cdc4367/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/ShelleyHFC.hs#L130-L164

There's probably plenty of redundancy that could be removed from this Consensus-side logic (in the "Inspect" module), but it's only exactly this one line that "ignores the ledger" https://github.com/input-output-hk/ouroboros-consensus/blob/51da3876c01edc2eec250fdc998f6cb33cdc4367/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/ShelleyHFC.hs#L160 shelleyAfterVoting is from the ledger state; it's incremented (here) whenever we apply a block.

Copy link
Member

Choose a reason for hiding this comment

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

I sounds like maybe you read "doesn't" because I wrote "does", which is a bit unusual.

I can't read 🤦 Sorry about that.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the links to the relevant snippets 🙏

but it's only exactly this one line that "ignores the ledger"

"Ignores the ledger when there aren't k blocks after a transition to an era was voted on the ledger, right?

- (2) The current HFC's behavior is inconsistent without making further assumptions.
Overall, ZEE is allowed, but it might cause some unexpected behavior.
In particular, forecasts always anticipate that there is at least one safe zone of slots in an era (which is not true for eras with zero epochs).
That mismatch could manifest as incorrect time-slot translations eg (within ledger rules execution, but not within Node-To-Client query reponses).
Copy link
Member

Choose a reason for hiding this comment

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

Could we elaborate an example of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could. It should be pretty simple, but it will require a lot of words, just because of the context. If it doesn't require a lot of works, then it would remain pretty abstract, which kind of defeats the purpose of the example.

I left it out for now because it seems like a lot of words. Maybe we could consolidate all of the examples into an "example" section that shares the necessary context amongst a few pointed examples.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think that would help the readers greatly. But I acknowledge this is not intended to be a document used for mass consumption, right? So perhaps this is also out of scope.


- (1) ZBE would be achievable for a prefix of eras when constructing the initial HFC ledger state (in the `ProtocolInfo` passed to the Consensus layer), but otherwise the HFC as a block type does not need to support ZBE at all.

- (2) Esgen points out that perhaps DSR could/should still be Insensitive, and that wouldn't obviously lose too much simplicity.
Copy link
Member

Choose a reason for hiding this comment

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

I wonder how that could even be possible. 🤔 Do you have an example?

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 think @amesgen would more easily be able to explain it than I can.

Copy link
Member

@amesgen amesgen Oct 12, 2023

Choose a reason for hiding this comment

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

Right now, the criterion for singleEraTransition to report the era transition for Shelley-based eras is:

Were there at least $k$ blocks after the voting deadline ($6k/f$ before an epoch boundary)?

We could change that to

Were there at least $k$ blocks after the voting deadline or was the last applied block within the last $3k/f$ slots of the epoch?

This choice would have DST=Sensitive, PIL=Impossible and DSR=Insensitive (apart from chain growth violations in the $3k/f$ slots after the voting deadline).

Copy link
Member

Choose a reason for hiding this comment

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

Were there at least $k$ blocks after the voting deadline ($6k/f$ before an epoch boundary)?

But don't we require 2k blocks after the voting deadline to actually transition to the next era?

Copy link
Member

Choose a reason for hiding this comment

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

This choice would have DST=Sensitive

Why? Because we would be counting slots?

PIL=Impossible

Because we would be counting slots, like the ledger does?

Copy link
Member

Choose a reason for hiding this comment

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

was the last applied block within the last $3k/f$ slots of the epoch?

and I don't get why this will ensure that a rollback won't cause a different era-transition decision (even if there are no CG violations).

(sorry for the many questions 🙏 )

There may be other points in the design space worth considering.
(Or even other dimensions, of course.)
In particular, DST=Insensitive and PIL=Impossible could be achieved by changing the Cardano ledger's governance to match the existing Cardano HFC logic.
Specifically, if the ledger did not enact governance actions unless there have actually been k+1 blocks since they were ratified even despite possible Chain Growth violations, then the Cardano HFC would never override the Cardano ledger.
Copy link
Member

Choose a reason for hiding this comment

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

I fail to see the relation between chain growth violations and k + 1 blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now governance actions are enacted if they both 1) get enough votes and 2) get those votes before a slot-based deadline. The specified choice of slot for the deadline has so far ensured that the final Yes-vs-No status of the governance action would be determined (ie predictable) more than k blocks before the change was actually made (aka "enacted")---unless there was a Chain Growth violation (since Chain Growth ensures at least k blocks of growth per a fixed number of slots).

Copy link
Member

Choose a reason for hiding this comment

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

Let me see if I get this: if $b^d_1$ is the block at the voting deadline, where the votes are counted, then in the following chain:

$b^d_1 \rightarrow b^d_2 \rightarrow \ldots \rightarrow b^d_k \rightarrow b^d_{k+1}$

The vote counting is not stable until we reach/add? block $b^d_{k+1}$, right?

And does the insensitivity to ticks stem from the fact that we're counting blocks instead of slots?

Copy link
Contributor Author

@nfrisby nfrisby Oct 15, 2023

Choose a reason for hiding this comment

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

The deadline is actually a boundary between slots. There's a slot that's the last slot you could sneak in a vote, and the next slot you can't. The boundary between them is the deadline.

If $b^d_1$ is the first block that is either in the slot immediately before that deadline or anywhere after it, then I think that diagram is correct. Because rolling back up to k blocks would still not allow the votes to change.


The vote counting is not stable until we reach/add? block $b^d_{k+1}$, right?

Hmm. The word "stable" is tricky, since it is often used to refer to something that's one stability window old. But in the presence of CG violations, that could still be subject to rollback.

So it's "actually stable" only once block k+1 arrives, yes.

And does the insensitivity to ticks stem from the fact that we're counting blocks instead of slots?

Yep.

- Assume k > 0.
- Let C be the current best valid chain in the net.
- Let D be the node's currently selected chain, and assume it's worse than C.
- Let L be the ledger state resulting from the youngest block that is on both C and D, aka their intersection.
Copy link
Member

@dnadales dnadales Oct 12, 2023

Choose a reason for hiding this comment

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

Now I realize that I'm confused about the mention of "youngest block", doesn't "block at the intersection" suffice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah; see our previous convo #346 (comment)

(This isn't necessarily true, due to tiebreakers.
However, even with tiebreakers, this remains true in the worst case, which is what ultimately matters.)
- Common Prefix ensures R <= k.
- L must contain enough information for the node to correctly judge the validity of the k+1th block after L on C.
Copy link
Member

@dnadales dnadales Oct 12, 2023

Choose a reason for hiding this comment

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

Not related to this PR, but why do we require k+1 instead of k? Because we won't be able to switch to C otherwise?

Copy link
Contributor Author

@nfrisby nfrisby Oct 15, 2023

Choose a reason for hiding this comment

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

Yep, that's why. Praos promises we'll never need to rollback more than k. And it's possible that (the tiebreakers align such that) we'd need to rollback exactly k in order to a longer chain, ie a chain with at least k+1 blocks after the intersection.

If we assume Chain Growth in addition to Common Prefix, then we can further refine the answer.

- Chain Growth ensures that every window of scg (ie "the s parameter of the papers' CG property", aka "one stability window") slots will contain at least k blocks on C.
- Because of the Header-Body Split, L must contain enough information for the node to correctly judge the validity of any header that is no more than 3k/f after the first block after L on C.
Copy link
Member

Choose a reason for hiding this comment

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

Why do we require that L can judge the validity of at least k + 1 blocks on any chain that forks from it, but judge the validity of 3k / f slots only? CG ensures we have at least k blocks, but we could have exactly k. Does that mean that we can only switch to chains that have > k blocks in 3k/f slots?

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, I think this is described below:

and hence k+1 blocks in total, as necessary.

Are we counting the intersection block as well? If so I'm still confused about the requirement that L can validate k + 1 blocks after the intersection 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can tick L up to the next block on their chain. And then we can forecast 3k/f from there.

I think that's actually exactly one slot short 🤔 of ensuring L can validate k+1 headers on a chain that does not violate without having to apply any more blocks to L, unless the sparsity of the k headers after the first violate Chain Growth.

What behaviors could arise only if Chain Growth were violated?

- For example, suppose R=k and the k+1th block after L on C was more than 3k/f slots after the first block after L on C.
- In this case, the node might not be able to switch from D to C despite R <= k.
Copy link
Member

Choose a reason for hiding this comment

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

I've drawn some pictures. I find they help me greatly in following the arguments presented here. Maybe we can add them to our documentation (but might not be needed for this PR).

image

And perhaps we should unify the notation for these diagrams 😄

- In this case, the node might not be able to switch from D to C despite R <= k.
It can't switch to C without downloading the R+1 blocks, which it can't do without validating the R+1 headers, which it can't do without predicting more than 3k/f slots after the first header, which it might not be able to do.

For example, suppose that L is the result of a block in relative slot number 7k/f - 2 within some epoch (ie "slightly more than scg before the end of the epoch").
Copy link
Member

Choose a reason for hiding this comment

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

Here is a bit of context I'm missing: do we assume a certain duration of the epochs in terms of 3k/f (or k/f)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes: Byron was 10k and every era after has been 10k/f slots per epoch. It could vary to other values in later eras, but no one within IOG has yet suggested it should.


For example, suppose that L is the result of a block in relative slot number 7k/f - 2 within some epoch (ie "slightly more than scg before the end of the epoch").
Because there might be a block in relative slot 7k/f - 1---which would change the nonce used for the next epoch---L is unable to validate headers in the next epoch.
If the first header after L on C is indeed in slot 7k/f - 1, then Chain Growth ensures the k+1th header after L on C will still be in this same epoch, and so L can validate it.
Copy link
Member

Choose a reason for hiding this comment

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

Without a drawing, it's quite hard for me to follow what L and C are. (I had to make one myself and I go to it back and forth, maybe we can spare the reader having to make her own drawing).

For example, suppose that L is the result of a block in relative slot number 7k/f - 2 within some epoch (ie "slightly more than scg before the end of the epoch").
Because there might be a block in relative slot 7k/f - 1---which would change the nonce used for the next epoch---L is unable to validate headers in the next epoch.
If the first header after L on C is indeed in slot 7k/f - 1, then Chain Growth ensures the k+1th header after L on C will still be in this same epoch, and so L can validate it.
If the first header after L on C is instead after slot 7k/f - 1, then ticking L to that first header will tick it past slot 7k/f - 1, and so determines the next epoch's nonce.
Copy link
Member

Choose a reason for hiding this comment

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

We would also need some more context/background info on when nonces are determined in relation to the slot numbers or division of epoch into chunks of k/f.

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's currently acquired 3k/f before the end of the epoch. IOG Researchers want us to move it slightly earlier, ie 4k/f before the end.


- No other behavior of the simplified node will change due to a Chain Growth violation.
(This is actually false, but only because we use predictions in the leadership check.
One option to make this true is to instead use ticking in the leadership check.
Copy link
Member

Choose a reason for hiding this comment

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

Why ticking would make this true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because you can only forecast so far, but you can tick indefinitely (in theory, at least). EG if there's a block in the slot 0 of an epoch, but there no block in the next 3k/f slots, then today's node wouldn't be able to lead even if it were elected in the next slot (ie slot 3k/f+1 of the epoch), since it couldn't forecast far enough to see that. On the other hand, it can tick to that slot.

(In theory ticking has no limit, but I think the Cardano ledger currently assumes you can't tick across more than one epoch boundary.)

- No other behavior of the simplified node will change due to a Chain Growth violation.
(This is actually false, but only because we use predictions in the leadership check.
One option to make this true is to instead use ticking in the leadership check.
Another option is to also characterize here how Chain Growth violations affect the predication-based leadership check, which is even milder than the above: it only manifests when there are zero blocks in some 3k/f slot window, not merely any number of blocks less than k.)
Copy link
Member

Choose a reason for hiding this comment

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

I don't get wahy this is the case. Would it be possible to illustrate this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Claim: "No other behavior of the simplified node will change due to a Chain Growth violation."

It's claiming that the intersection of A and B is empty, where A is "behaviors that only arise in the presence of CG violations" and B is "behaviors that haven't already been discussed".

  • If we change the leadership check to use ticking, then CG violations wouldn't affect it, and so the above claim would actually be true, since the leadership check is no longer member of A.

  • If we instead also explained that (very extreme) CG violations can affect the leadership check, then the above claim would actually be true, since the leadership check is no longer a member of B.

@IntersectMBO IntersectMBO deleted a comment from dnadales Oct 15, 2023
Unlike the ledger, the hard fork combinator actually counts blocks instead of only slots.
If a hypothetical roll back of k-many blocks could possibly change the ledger-based governance decision that was intended to trigger the era transition---which is true if Chain Growth was (severely) violated in the last two stability windows before the intended era transition---then the hard fork combinator will not transition to the next era.
(For this reason, the voting deadline was moved from 7k/f to 4k/f (ie two stablity windows before the end) in the ledger governance rules, so that the hard fork combinator's final decision would be known at least a stability window (and so the safe zone) before the epoch transition.)
Awkwardly, the ledger will still update the major version of the protocol version protocol parameter, but the chain will remain in the previous era, which many users and developers would find counter-intuitive.
Copy link
Member

Choose a reason for hiding this comment

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

This section makes more sense after I read your answers to my questions. Also the second draft helped me a lot.


Consider the following alteration of the hard fork combinator.

- Remove the hard fork combinator's second trigger constraint, such that it transitions to the next era exactly when the ledger governance updates the major version of the protocol version protocol parameter (mutatis mutandi as of Conway).
Copy link
Member

Choose a reason for hiding this comment

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

What is the first trigger constraint? 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That the ledger governance decided the protocol major version would change.


There are three main benefits for the Consensus Team.

- The hard fork combinator would no longer need the block-counting logic.
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean that, when checking era transitions we'd be counting slots instead?

There are three main benefits for the Consensus Team.

- The hard fork combinator would no longer need the block-counting logic.
- The Consensus Team would no longer have to justify the "double-stability" for the ledger governance rules.
Copy link
Member

Choose a reason for hiding this comment

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

I don't follow this. Even if we rely on the ledger's decision, we'd still need a double stability to ensure time-translations are sound. If not, a rollback could give a different answer to a time-translation.

@@ -0,0 +1,285 @@
# DRAFT NUMBER 2
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We suspect we should rename the .md file, as of this second draft

@dnadales dnadales assigned nfrisby and unassigned dnadales Oct 30, 2023
@nfrisby nfrisby mentioned this pull request Jan 10, 2024
5 tasks
@dnadales dnadales mentioned this pull request Jan 24, 2024
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 👀 In review
Development

Successfully merging this pull request may close these issues.

3 participants