-
Notifications
You must be signed in to change notification settings - Fork 9
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
Thv/external guessers #478
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't comment on the architectural design since I'm too unfamiliar with the architecture of Neptune core. The construction can benefit from a few additional iterations.
// This block spans global state write lock for updating. | ||
let mut global_state_mut = self.global_state_lock.lock_guard_mut().await; | ||
|
||
if !global_state_mut.incoming_block_is_more_canonical(&new_block) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern if !some_bool { a() } else { b() }
inhibits readability. I suggest to either use if some_bool { b() } else { a() }
or to name the boolean such that negation happens somewhere else.
let Some(mut proposal) = self | ||
.state | ||
.lock_guard() | ||
.await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lock guard is acquired multiple times. Can this be factored out into a variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's intentional. To return as quickly as possible and release all locks as quickly as possible.
let Some(proposal) = self | ||
.state | ||
.lock_guard() | ||
.await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lock guard is acquired multiple times. Can this be factored out into a variable?
src/rpc_server.rs
Outdated
let (guesser_key_after_image, latest_block_header) = { | ||
let state = self.state.lock_guard().await; | ||
let guesser_key_after_image = state | ||
.wallet_state | ||
.wallet_secret | ||
.guesser_spending_key(proposal.header().prev_block_digest); | ||
let latest_block_header = *state.chain.light_state().header(); | ||
|
||
(guesser_key_after_image.after_image(), latest_block_header) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why so roundabout?
let (guesser_key_after_image, latest_block_header) = { | |
let state = self.state.lock_guard().await; | |
let guesser_key_after_image = state | |
.wallet_state | |
.wallet_secret | |
.guesser_spending_key(proposal.header().prev_block_digest); | |
let latest_block_header = *state.chain.light_state().header(); | |
(guesser_key_after_image.after_image(), latest_block_header) | |
}; | |
let state = self.state.lock_guard().await; | |
let guesser_key_after_image = state | |
.wallet_state | |
.wallet_secret | |
.guesser_spending_key(proposal.header().prev_block_digest) | |
.after_image(); | |
let latest_block_header = *state.chain.light_state().header(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why so roundabout?
just some background:
The first way drops the lock guard when the end of (inner) scope is reached, releasing the lock.
Your suggestion keeps the lock around until the end of outer scope. It can be explicitly dropped with drop(state)
but the suggestion does not do that, so it actually changes behavior (assuming there is subsequent code in the outer scope).
We always want to drop the locks as quickly as possible after usage, and never hold them across lengthy operations, or a second attempt to acquire the lock. There are basically 3 ways to acquire/use/drop:
- in a single statement:
global_state_lock.lock_guard().do_something()
- in a new scope:
{let state = global_state_lock.lock_guard(); do_something()}
- with a drop:
let state = global_state_lock.lock_guard(); do_something(); drop(state);
I tend to prefer (1) and (2) and use (3) somewhat sparingly. I find that (3) is more likely to lead to drop() being forgotten or later removed, while (2) makes the drop automatic and impossible to forget.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The advantage of an explicit drop(…)
is that is communicates what is happening instead of relying on implicit behavior. I think this helps readability and maintainability; scoping might simplify initial construction.
I personally am less likely to remove a drop(…)
than I am to rewrite an oddly scoped block that looks like an artifact of a refactoring tool (in this case, inlining a function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are valid points. However removing a drop() during maintenance is only half of it. The other half is remembering to put it there in the first place, and to keep it in the right place as code is refactored. If one uses the convention that every time we acquire a lock guard for use by 2 or more statements we start a new scope, then there is less chance of forgetting to add the drop, and unwittingly hold it across a lengthy operation.
Either way, it is pattern/convention within the codebase that requires some familiarity to get right. Comments are helpful, especially for new eyes to the codebase. And we should always pay extra attention to any lock changes in PRs.
let Some(proposal) = self | ||
.state | ||
.lock_guard() | ||
.await | ||
.block_proposal | ||
.map(|x| x.to_owned()) | ||
else { | ||
return Ok(None); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let Some(proposal) = self | |
.state | |
.lock_guard() | |
.await | |
.block_proposal | |
.map(|x| x.to_owned()) | |
else { | |
return Ok(None); | |
}; | |
let state = self.state.lock_guard().await; | |
let Some(proposal) = state.block_proposal.map(|x| x.to_owned()) else { | |
return Ok(None); | |
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, be mindful that the suggested change is not dropping the acquired lock. I haven't checked subsequent code, so maybe it's ok, but it is a logic change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The word "challenge" to describe the information needed to completely specify a computational task and test candidate solutions is a poor choice of words and I strongly suggest finding an alternative.
In computational complexity and in cryptography the word used to describe this information is problem. More specifically it is a search problem (as opposed to a decision problem; because the output is more than one bit). Opinions differ about what the thing being searched for ought to be called, but the feature of hash functions that makes them suitable for use in proof-of-work is correlation intractability. That said, I'm not sure a scientifically accurate term is the best choice of words for type, variable, and function names.
In the context of proof-of-work, and particularly in less scientific and more layperson-friendly literature, I've seen "proof-of-work puzzle" used often. I suggest to go with this phrase.
The reason why "challenge" is a poor choice is because in cryptography and decentralized protocols it denotes one step of an interactive challenge and response protocol. A verifier sets the challenge and scrutinizes the response in order to verify the authenticity of prior or subsequent messages, or the authorization of the responder. These features do not describe proof-of-work.
src/rpc_server.rs
Outdated
/// Provide a PoW-solution to the current block proposal. | ||
/// | ||
/// Returns true iff the provided solution satisfies the proof-of-work | ||
/// threshold of the next block. False otherwise. False can be returned if | ||
/// the solution comes in too late in which case either a new block | ||
/// proposal is stored by the client, or no block proposal is known at all. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or*
With that correction I prefer Ferdinand's phrasing.
About the part where false
is returned when there is a new block proposal -- is that really the desired behavior? IIuc, it happens when
- the RPC client has a valid solution to the proof-of-work puzzle, but
neptune-core
has already switched to a new block proposal, probably because it is more favorable.
Isn't the 100% certainty of getting the less favorable guesser reward preferable to X% certainty of getting the more favorable guesser reward? It is possible that X (the proportion of the guessing power owned by the user) and the favorability difference is such that it is rational to ignore own proof-of-work solutions, but it seems like a very poor heuristic to default to.
}; | ||
|
||
// A proposal was found. Check if solution works. | ||
let (guesser_key_after_image, latest_block_header) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Binding a value of type SpendingKey
to a variable named *_after_image
seems like a category error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. You're calling .after_image()
which makes the value bound to this variable of type Digest
. So that checks out.
I'm sure there is a less confusing way of structuring the code, or maybe even just variable naming. At any rate, the same criticism survives for the variable name two lines lower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with the variable-renaming. The code, though, is structured this way to minimize the time that read locks are held ,and to using scoping rules to release locks.
|
||
// No time to waste! Inform main_loop! | ||
let solution = Box::new(proposal); | ||
let _ = self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is not much you can do besides
- killing the RPC server task (probably not a good idea since the channel might be temporarily out of capacity)
- logging an error message (probably a good idea).
src/rpc_server.rs
Outdated
} | ||
|
||
#[tokio::test] | ||
async fn exported_guess_challenge_is_consistent_with_block_hash() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "exported" the right word? When I hear it I think of "export to PDF".
What does this test achieve that guess_challenge_info_is_consistent_with_block_hash
does not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guess_challenge_info_is_consistent_with_block_hash
(two tests above) tests the logic of the data structure holding the data of the puzzle. This test tests the entire pipeline.
Add an RPC endpoint that allows for the exporting of the guesser challenge to an external program.
Add endpoint `pow_solution` to provide a solution to the proof-of-work challenge. This can be used by external programs or networks that do the guessing.
6001e8c
to
16b92aa
Compare
Integrated most reviewer feedback. Still releasing locks as soon as possible, though. Let's discuss the issue @aszepieniec raised: What to do with the raise condition where the client registers a new block proposal (not a new block) and the external guesser (simultaneously) finds the block for the old block proposal that no longer is stored in the client's state. The solution I've come up with is to store exported block proposals to a file somewhere. |
Add infrastructure (RPC endpoints) to allow guessing by other programs than neptune-core. Still missing: CLI endpoints for this feature, and callbacks when a new block proposal is received. But this is a good start.