Skip to content

Commit

Permalink
feat(rpc_server): Add endpoint to provide PoW solution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Sword-Smith committed Feb 28, 2025
1 parent ee1c723 commit 6001e8c
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 38 deletions.
83 changes: 50 additions & 33 deletions src/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::macros::log_slow_scope;
use crate::models::blockchain::block::block_header::BlockHeader;
use crate::models::blockchain::block::block_height::BlockHeight;
use crate::models::blockchain::block::difficulty_control::ProofOfWork;
use crate::models::blockchain::block::Block;
use crate::models::blockchain::transaction::Transaction;
use crate::models::blockchain::transaction::TransactionProof;
use crate::models::channel::MainToMiner;
Expand Down Expand Up @@ -485,6 +486,41 @@ impl MainLoopHandler {
self.main_to_miner_tx.send(MainToMiner::Continue);
}

async fn handle_self_guessed_block(
&mut self,
main_loop_state: &mut MutableMainLoopState,
new_block: Box<Block>,
) -> Result<()> {
// Store block in database
// 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) {
drop(global_state_mut); // don't hold across send()
warn!("Got new block from miner that was not child of tip. Discarding.");
self.main_to_miner_tx.send(MainToMiner::Continue);
return Ok(());
} else {
info!("Locally-mined block is new tip: {}", new_block.hash());
}

// Share block with peers first thing.
info!("broadcasting new block to peers");
self.main_to_peer_broadcast_tx
.send(MainToPeerTask::Block(new_block.clone()))
.expect("Peer handler broadcast channel prematurely closed. This should never happen.");

let update_jobs = global_state_mut
.set_new_tip(new_block.as_ref().clone())
.await?;
drop(global_state_mut);

self.spawn_mempool_txs_update_job(main_loop_state, update_jobs)
.await;

Ok(())
}

/// Locking:
/// * acquires `global_state_lock` for write
async fn handle_miner_task_message(
Expand All @@ -499,38 +535,8 @@ impl MainLoopHandler {
let new_block = new_block_info.block;

info!("Miner found new block: {}", new_block.kernel.header.height);

// Store block in database
// 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) {
drop(global_state_mut); // don't hold across send()
warn!("Got new block from miner task that was not child of tip. Discarding.");
self.main_to_miner_tx.send(MainToMiner::Continue);
return Ok(None);
} else {
info!(
"Block from miner is new canonical tip: {}",
new_block.hash(),
);
}

// Share block with peers first thing.
info!("broadcasting new block to peers");
self.main_to_peer_broadcast_tx
.send(MainToPeerTask::Block(new_block.clone()))
.expect(
"Peer handler broadcast channel prematurely closed. This should never happen.",
);

let update_jobs = global_state_mut
.set_new_tip(new_block.as_ref().clone())
self.handle_self_guessed_block(main_loop_state, new_block)
.await?;
drop(global_state_mut);

self.spawn_mempool_txs_update_job(main_loop_state, update_jobs)
.await;
}
MinerToMain::BlockProposal(boxed_proposal) => {
let (block, expected_utxos) = *boxed_proposal;
Expand Down Expand Up @@ -1526,7 +1532,7 @@ impl MainLoopHandler {

// Handle messages from rpc server task
Some(rpc_server_message) = rpc_server_to_main_rx.recv() => {
let shutdown_after_execution = self.handle_rpc_server_message(rpc_server_message.clone()).await?;
let shutdown_after_execution = self.handle_rpc_server_message(rpc_server_message.clone(), &mut main_loop_state).await?;
if shutdown_after_execution {
break SUCCESS_EXIT_CODE
}
Expand Down Expand Up @@ -1616,7 +1622,11 @@ impl MainLoopHandler {

/// Handle messages from the RPC server. Returns `true` iff the client should shut down
/// after handling this message.
async fn handle_rpc_server_message(&mut self, msg: RPCServerToMain) -> Result<bool> {
async fn handle_rpc_server_message(
&mut self,
msg: RPCServerToMain,
main_loop_state: &mut MutableMainLoopState,
) -> Result<bool> {
match msg {
RPCServerToMain::BroadcastTx(transaction) => {
debug!(
Expand Down Expand Up @@ -1679,6 +1689,13 @@ impl MainLoopHandler {
// do not shut down
Ok(false)
}
RPCServerToMain::SolvePow(new_block) => {
info!("Handling PoW solution from RPC call");

self.handle_self_guessed_block(main_loop_state, new_block)
.await?;
Ok(false)
}
RPCServerToMain::PauseMiner => {
info!("Received RPC request to stop miner");

Expand Down
1 change: 1 addition & 0 deletions src/models/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ pub(crate) struct ClaimUtxoData {
#[derive(Clone, Debug)]
pub(crate) enum RPCServerToMain {
BroadcastTx(Box<Transaction>),
SolvePow(Box<Block>),
Shutdown,
PauseMiner,
RestartMiner,
Expand Down
140 changes: 135 additions & 5 deletions src/rpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,14 @@ pub trait RPC {
/// ```
async fn restart_miner(token: rpc_auth::Token) -> RpcResult<()>;

/// 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.
async fn pow_solution(token: rpc_auth::Token, nonce: Digest) -> RpcResult<bool>;

/// mark MUTXOs as abandoned
///
/// ```no_run
Expand Down Expand Up @@ -3103,6 +3111,64 @@ impl RPC for NeptuneRPCServer {
Ok(())
}

// documented in trait. do not add doc-comment.
async fn pow_solution(
self,
_context: tarpc::context::Context,
token: rpc_auth::Token,
nonce: Digest,
) -> RpcResult<bool> {
log_slow_scope!(fn_name!());
token.auth(&self.valid_tokens)?;

let Some(mut proposal) = self
.state
.lock_guard()
.await
.block_proposal
.map(|x| x.to_owned())
else {
warn!(
"Got claimed PoW solution but no challenge was known. \
Did solution come in too late?"
);
return Ok(false);
};

// A proposal was found. Check if solution works.
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)
};

proposal.set_header_guesser_digest(guesser_key_after_image);
proposal.set_header_nonce(nonce);
let threshold = latest_block_header.difficulty.target();
let solution_digest = proposal.hash();
if solution_digest > threshold {
warn!(
"Got claimed PoW solution but PoW threshold was not met.\n\
Claimed solution: {solution_digest};\nthreshold: {threshold}"
);
return Ok(false);
}

// No time to waste! Inform main_loop!
let solution = Box::new(proposal);
let _ = self
.rpc_server_to_main_tx
.send(RPCServerToMain::SolvePow(solution))
.await;

Ok(true)
}

// documented in trait. do not add doc-comment.
async fn prune_abandoned_monitored_utxos(
mut self,
Expand Down Expand Up @@ -4293,6 +4359,29 @@ mod rpc_server_tests {
assert_eq!(block1.hash(), challenge_guess);
}

#[tokio::test]
async fn guess_with_no_proposal() {
let network = Network::Main;
let bob = test_rpc_server(
network,
WalletSecret::new_random(),
2,
cli_args::Args::default(),
)
.await;
let bob_token = cookie_token(&bob).await;
assert!(!bob.state.lock_guard().await.block_proposal.is_some());
let accepted = bob
.clone()
.pow_solution(context::current(), bob_token, random())
.await
.unwrap();
assert!(
!accepted,
"Must reject PoW solution when no proposal exists"
);
}

#[tokio::test]
async fn exported_guess_challenge_is_consistent_with_block_hash() {
let network = Network::Main;
Expand All @@ -4313,14 +4402,14 @@ mod rpc_server_tests {
.unwrap()
.unwrap();

let nonce = random();
let challenge_guess = fast_kernel_mast_hash(
let mock_nonce = random();
let mut challenge_guess = fast_kernel_mast_hash(
guess_challenge.kernel_auth_path,
guess_challenge.header_auth_path,
nonce,
mock_nonce,
);

block1.set_header_nonce(nonce);
block1.set_header_nonce(mock_nonce);

let expected_guesser_digest = bob
.state
Expand All @@ -4338,7 +4427,48 @@ mod rpc_server_tests {
guess_challenge.total_guesser_reward
);

// TODO: Check that successfull guess is registered by wallet.
// Check that succesful guess is accepted by endpoint.
let actual_threshold = genesis.header().difficulty.target();
let mut actual_nonce = mock_nonce;
while challenge_guess > actual_threshold {
actual_nonce = random();
challenge_guess = fast_kernel_mast_hash(
guess_challenge.kernel_auth_path,
guess_challenge.header_auth_path,
actual_nonce,
);
}

block1.set_header_nonce(actual_nonce);
let good_is_accepted = bob
.clone()
.pow_solution(context::current(), bob_token, actual_nonce)
.await
.unwrap();
assert!(
good_is_accepted,
"Actual PoW solution must be accepted by RPC endpoint."
);

// Check that bad guess is rejected by endpoint.
let mut bad_nonce: Digest = actual_nonce;
while challenge_guess <= actual_threshold {
bad_nonce = random();
challenge_guess = fast_kernel_mast_hash(
guess_challenge.kernel_auth_path,
guess_challenge.header_auth_path,
bad_nonce,
);
}
let bad_is_accepted = bob
.clone()
.pow_solution(context::current(), bob_token, bad_nonce)
.await
.unwrap();
assert!(
!bad_is_accepted,
"Bad PoW solution must be rejected by RPC endpoint."
);
}
}

Expand Down

0 comments on commit 6001e8c

Please sign in to comment.