-
Notifications
You must be signed in to change notification settings - Fork 11
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.
src/rpc_server.rs
Outdated
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.
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 variable rename dispels all the confusion I was struggling with. 👍
|
||
// 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.
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. |
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 miners potentially expend a lot of resources on guessing infrastructure. So when a a successful guess cannot be sent to the main thread (for whatever reason) it would be costly to let that information disappear. Rather than letting it disappear, I suggest to log an error message that contains the proof-of-work solution.
If I'm not mistaken the test for handle_self_guessed_block
is yet to be written, is that correct?
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.
Well phrased.
The solution I've come up with is to store exported block proposals to a file somewhere.
That's a fine solution.
Here are two alternatives worth considering:
- Store it in RAM, on the state of the RPC server.
- Send the block proposal and send the block back, instead of the the proof-of-work puzzle and the solution.
/// Process a block whose PoW solution was solved by this client (or an | ||
/// external program) and has not been seen by the rest of the network yet. | ||
/// | ||
/// Shares block with all connected peers, updates own state, and updates | ||
/// any mempool transactions to be valid under this new block. | ||
/// | ||
/// Locking: | ||
/// * acquires `global_state_lock` for write | ||
async fn handle_self_guessed_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.
Excellent docstring 🎬
Where do I find the test of this 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.
Good question! I just added one in c48b2f1.
}; | ||
|
||
// 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.
The variable rename dispels all the confusion I was struggling with. 👍
16b92aa
to
c48b2f1
Compare
7531dd1
to
2b395e6
Compare
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.
02d77c7
to
5e8651b
Compare
In commit 1d145d3, the handling of a new block by `main_loop` is factored out to a separate function, since this logic can now be invoked in two cases: If the node's own guesser finds a solution to the PoW-puzzle, and if an external program (like a GPU guesser) finds a solution to the PoW-puzzle. Previously, this logic could only be invoked by the node's own guesser. But this factored out function was not tested. This commit fixes that, at least for the happy path of this function.
To avoid valid PoW solutions being rejected because a new, more favorable block proposal has been received, the set of all exported pow-puzzles (for the relevant height) is stored in a hash map. This hash map is cleared every time a new block comes in, since we don't care about puzzles for lower block heights than tip.
Add a test ensuring that block proposal and the set of exported block proposal for external guessers is cleared when a new block is set as tip.
5e8651b
to
b693014
Compare
Ready for review again. Now all block exported block proposals are stored in RAM. This set is cleared, though, whenever a new tip is set. Also added test of |
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.