rpc_miner.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{collections::HashMap, str::FromStr};
  19. use darkfi::{
  20. blockchain::{BlockInfo, Header, HeaderHash},
  21. rpc::jsonrpc::{ErrorCode, ErrorCode::InvalidParams, JsonError, JsonResponse, JsonResult},
  22. tx::{ContractCallLeaf, Transaction, TransactionBuilder},
  23. util::{encoding::base64, time::Timestamp},
  24. validator::{
  25. consensus::{Fork, Proposal},
  26. pow::{RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT},
  27. verification::apply_producer_transaction,
  28. },
  29. zk::ProvingKey,
  30. zkas::ZkBinary,
  31. Error, Result,
  32. };
  33. use darkfi_money_contract::{client::pow_reward_v1::PoWRewardCallBuilder, MoneyFunction};
  34. use darkfi_sdk::{
  35. crypto::{
  36. keypair::{Address, Keypair, SecretKey},
  37. pasta_prelude::PrimeField,
  38. FuncId, MerkleTree, MONEY_CONTRACT_ID,
  39. },
  40. pasta::pallas,
  41. ContractCall,
  42. };
  43. use darkfi_serial::{serialize_async, Encodable};
  44. use num_bigint::BigUint;
  45. use rand::rngs::OsRng;
  46. use tinyjson::JsonValue;
  47. use tracing::{error, info};
  48. use crate::{proto::ProposalMessage, server_error, DarkfiNode, RpcError};
  49. /// Auxiliary structure representing node miner rewards recipient configuration.
  50. pub struct MinerRewardsRecipientConfig {
  51. /// Wallet mining address to receive mining rewards
  52. pub recipient: Address,
  53. /// Optional contract spend hook to use in the mining reward
  54. pub spend_hook: Option<FuncId>,
  55. /// Optional contract user data to use in the mining reward.
  56. /// This is not arbitrary data.
  57. pub user_data: Option<pallas::Base>,
  58. }
  59. /// Auxiliary structure representing a block template for native mining.
  60. pub struct BlockTemplate {
  61. /// The block that is being mined
  62. pub block: BlockInfo,
  63. /// The base64 encoded RandomX key used
  64. randomx_key: String,
  65. /// The base64 encoded mining target used
  66. target: String,
  67. /// The signing secret for this block
  68. secret: SecretKey,
  69. }
  70. impl DarkfiNode {
  71. // RPCAPI:
  72. // Queries the validator for the current mining RandomX key,
  73. // based on next block height.
  74. // If no forks exist, returns the canonical key.
  75. // Returns the current mining RandomX key encoded as base64 string.
  76. //
  77. // **Params:**
  78. // * `None`
  79. //
  80. // **Returns:**
  81. // * `String`: Current mining RandomX key (base64 encoded)
  82. //
  83. // --> {"jsonrpc": "2.0", "method": "miner.get_current_mining_randomx_key", "params": [], "id": 1}
  84. // <-- {"jsonrpc": "2.0", "result": ["randomx_key"], "id": 1}
  85. pub async fn miner_get_current_mining_randomx_key(
  86. &self,
  87. id: u16,
  88. params: JsonValue,
  89. ) -> JsonResult {
  90. // Verify request params
  91. let Some(params) = params.get::<Vec<JsonValue>>() else {
  92. return JsonError::new(InvalidParams, None, id).into()
  93. };
  94. if !params.is_empty() {
  95. return JsonError::new(InvalidParams, None, id).into()
  96. }
  97. // Grab current mining RandomX key
  98. let randomx_key = match self.validator.current_mining_randomx_key().await {
  99. Ok(key) => key,
  100. Err(e) => {
  101. error!(
  102. target: "darkfid::rpc::current_mining_randomx_key",
  103. "[RPC] Retrieving current mining RandomX key failed: {e}",
  104. );
  105. return JsonError::new(ErrorCode::InternalError, None, id).into()
  106. }
  107. };
  108. // Encode them and build response
  109. let response = JsonValue::Array(vec![JsonValue::String(base64::encode(
  110. &serialize_async(&randomx_key).await,
  111. ))]);
  112. JsonResponse::new(response, id).into()
  113. }
  114. // RPCAPI:
  115. // Queries the validator for the current best fork next header to
  116. // mine.
  117. // Returns the current RandomX key, the mining target and the next
  118. // block header, all encoded as base64 strings.
  119. //
  120. // **Params:**
  121. // * `header` : Mining job Header hash that is currently being polled (as string)
  122. // * `recipient` : Wallet mining address to receive mining rewards (as string)
  123. // * `spend_hook`: Optional contract spend hook to use in the mining reward (as string)
  124. // * `user_data` : Optional contract user data (not arbitrary data) to use in the mining reward (as string)
  125. //
  126. // **Returns:**
  127. // * `String`: Current best fork RandomX key (base64 encoded)
  128. // * `String`: Current best fork mining target (base64 encoded)
  129. // * `String`: Current best fork next block header (base64 encoded)
  130. //
  131. // --> {"jsonrpc": "2.0", "method": "miner.get_header", "params": {"header": "hash", "recipient": "address"}, "id": 1}
  132. // <-- {"jsonrpc": "2.0", "result": ["randomx_key", "target", "header"], "id": 1}
  133. pub async fn miner_get_header(&self, id: u16, params: JsonValue) -> JsonResult {
  134. // Check if node is synced before responding to miner
  135. if !*self.validator.synced.read().await {
  136. return server_error(RpcError::NotSynced, id, None)
  137. }
  138. // Parse request params
  139. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  140. return JsonError::new(InvalidParams, None, id).into()
  141. };
  142. if params.len() < 2 || params.len() > 4 {
  143. return JsonError::new(InvalidParams, None, id).into()
  144. }
  145. // Parse header hash
  146. let Some(header_hash) = params.get("header") else {
  147. return server_error(RpcError::MinerMissingHeader, id, None)
  148. };
  149. let Some(header_hash) = header_hash.get::<String>() else {
  150. return server_error(RpcError::MinerInvalidHeader, id, None)
  151. };
  152. let Ok(header_hash) = HeaderHash::from_str(header_hash) else {
  153. return server_error(RpcError::MinerInvalidHeader, id, None)
  154. };
  155. // Parse recipient wallet address
  156. let Some(recipient) = params.get("recipient") else {
  157. return server_error(RpcError::MinerMissingRecipient, id, None)
  158. };
  159. let Some(recipient_str) = recipient.get::<String>() else {
  160. return server_error(RpcError::MinerInvalidRecipient, id, None)
  161. };
  162. let Ok(recipient) = Address::from_str(recipient_str) else {
  163. return server_error(RpcError::MinerInvalidRecipient, id, None)
  164. };
  165. if recipient.network() != self.network {
  166. return server_error(RpcError::MinerInvalidRecipientPrefix, id, None)
  167. };
  168. // Parse spend hook
  169. let spend_hook = match params.get("spend_hook") {
  170. Some(spend_hook) => {
  171. let Some(spend_hook) = spend_hook.get::<String>() else {
  172. return server_error(RpcError::MinerInvalidSpendHook, id, None)
  173. };
  174. let Ok(spend_hook) = FuncId::from_str(spend_hook) else {
  175. return server_error(RpcError::MinerInvalidSpendHook, id, None)
  176. };
  177. Some(spend_hook)
  178. }
  179. None => None,
  180. };
  181. // Parse user data
  182. let user_data: Option<pallas::Base> = match params.get("user_data") {
  183. Some(user_data) => {
  184. let Some(user_data) = user_data.get::<String>() else {
  185. return server_error(RpcError::MinerInvalidUserData, id, None)
  186. };
  187. let Ok(bytes) = bs58::decode(&user_data).into_vec() else {
  188. return server_error(RpcError::MinerInvalidUserData, id, None)
  189. };
  190. let bytes: [u8; 32] = match bytes.try_into() {
  191. Ok(b) => b,
  192. Err(_) => return server_error(RpcError::MinerInvalidUserData, id, None),
  193. };
  194. let Some(user_data) = pallas::Base::from_repr(bytes).into() else {
  195. return server_error(RpcError::MinerInvalidUserData, id, None)
  196. };
  197. Some(user_data)
  198. }
  199. None => None,
  200. };
  201. // Now that method params format is correct, we can check if we
  202. // already have a mining job for this wallet. If we already
  203. // have it, we check if the fork it extends is still the best
  204. // one. If both checks pass, we can just return an empty
  205. // response if the request `aux_hash` matches the job one,
  206. // otherwise return the job block template hash. In case the
  207. // best fork has changed, we drop this job and generate a
  208. // new one. If we don't know this wallet, we create a new job.
  209. // We'll also obtain a lock here to avoid getting polled
  210. // multiple times and potentially missing a job. The lock is
  211. // released when this function exits.
  212. let address_bytes =
  213. serialize_async(&(recipient_str.clone().into_bytes(), spend_hook, user_data)).await;
  214. let mut blocktemplates = self.blocktemplates.lock().await;
  215. let mut extended_fork = match self.validator.best_current_fork().await {
  216. Ok(f) => f,
  217. Err(e) => {
  218. error!(
  219. target: "darkfid::rpc::miner_get_header",
  220. "[RPC] Finding best fork index failed: {e}",
  221. );
  222. return JsonError::new(ErrorCode::InternalError, None, id).into()
  223. }
  224. };
  225. if let Some(blocktemplate) = blocktemplates.get(&address_bytes) {
  226. let last_proposal = match extended_fork.last_proposal() {
  227. Ok(p) => p,
  228. Err(e) => {
  229. error!(
  230. target: "darkfid::rpc::miner_get_header",
  231. "[RPC] Retrieving best fork last proposal failed: {e}",
  232. );
  233. return JsonError::new(ErrorCode::InternalError, None, id).into()
  234. }
  235. };
  236. if last_proposal.hash == blocktemplate.block.header.previous {
  237. return if blocktemplate.block.header.hash() != header_hash {
  238. JsonResponse::new(
  239. JsonValue::Array(vec![
  240. JsonValue::String(blocktemplate.randomx_key.clone()),
  241. JsonValue::String(blocktemplate.target.clone()),
  242. JsonValue::String(base64::encode(
  243. &serialize_async(&blocktemplate.block.header).await,
  244. )),
  245. ]),
  246. id,
  247. )
  248. .into()
  249. } else {
  250. JsonResponse::new(JsonValue::Array(vec![]), id).into()
  251. }
  252. }
  253. blocktemplates.remove(&address_bytes);
  254. }
  255. // At this point, we should query the Validator for a new blocktemplate.
  256. // We first need to construct `MinerRewardsRecipientConfig` from the
  257. // address configuration provided to us through the RPC.
  258. let spend_hook_str = match spend_hook {
  259. Some(spend_hook) => format!("{spend_hook}"),
  260. None => String::from("-"),
  261. };
  262. let user_data_str = match user_data {
  263. Some(user_data) => bs58::encode(user_data.to_repr()).into_string(),
  264. None => String::from("-"),
  265. };
  266. let recipient_config = MinerRewardsRecipientConfig { recipient, spend_hook, user_data };
  267. // Now let's try to construct the blocktemplate.
  268. let (target, block, secret) = match generate_next_block(
  269. &mut extended_fork,
  270. &recipient_config,
  271. &self.powrewardv1_zk.zkbin,
  272. &self.powrewardv1_zk.provingkey,
  273. self.validator.consensus.module.read().await.target,
  274. self.validator.verify_fees,
  275. )
  276. .await
  277. {
  278. Ok(v) => v,
  279. Err(e) => {
  280. error!(
  281. target: "darkfid::rpc::miner_get_header",
  282. "[RPC] Failed to generate next blocktemplate: {e}",
  283. );
  284. return JsonError::new(ErrorCode::InternalError, None, id).into()
  285. }
  286. };
  287. // Grab the RandomX key to use.
  288. // We only use the next key when the next block is the
  289. // height changing one.
  290. let randomx_key = if block.header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
  291. block.header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
  292. {
  293. base64::encode(&serialize_async(&extended_fork.module.darkfi_rx_keys.1).await)
  294. } else {
  295. base64::encode(&serialize_async(&extended_fork.module.darkfi_rx_keys.0).await)
  296. };
  297. // Convert the target
  298. let target = base64::encode(&target.to_bytes_le());
  299. // Construct the block template
  300. let blocktemplate = BlockTemplate {
  301. block,
  302. randomx_key: randomx_key.clone(),
  303. target: target.clone(),
  304. secret,
  305. };
  306. // Now we have the blocktemplate. We'll mark it down in memory,
  307. // and then ship it to RPC.
  308. let header_hash = blocktemplate.block.header.hash().to_string();
  309. let header = base64::encode(&serialize_async(&blocktemplate.block.header).await);
  310. blocktemplates.insert(address_bytes, blocktemplate);
  311. info!(
  312. target: "darkfid::rpc::miner_get_header",
  313. "[RPC] Created new blocktemplate: address={recipient_str}, spend_hook={spend_hook_str}, user_data={user_data_str}, hash={header_hash}"
  314. );
  315. let response = JsonValue::Array(vec![
  316. JsonValue::String(randomx_key),
  317. JsonValue::String(target),
  318. JsonValue::String(header),
  319. ]);
  320. JsonResponse::new(response, id).into()
  321. }
  322. // RPCAPI:
  323. // Submits a PoW solution header nonce for a block.
  324. // Returns the block submittion status.
  325. //
  326. // **Params:**
  327. // * `recipient` : Wallet mining address used (as string)
  328. // * `spend_hook`: Optional contract spend hook used (as string)
  329. // * `user_data` : Optional contract user data (not arbitrary data) used (as string)
  330. // * `nonce` : The solution header nonce (as f64)
  331. //
  332. // **Returns:**
  333. // * `String`: Block submit status
  334. //
  335. // --> {"jsonrpc": "2.0", "method": "miner.submit_solution", "params": {"recipient": "address", "nonce": 42}, "id": 1}
  336. // <-- {"jsonrpc": "2.0", "result": "accepted", "id": 1}
  337. pub async fn miner_submit_solution(&self, id: u16, params: JsonValue) -> JsonResult {
  338. // Check if node is synced before responding to p2pool
  339. if !*self.validator.synced.read().await {
  340. return server_error(RpcError::NotSynced, id, None)
  341. }
  342. // Parse request params
  343. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  344. return JsonError::new(InvalidParams, None, id).into()
  345. };
  346. if params.len() < 2 || params.len() > 4 {
  347. return JsonError::new(InvalidParams, None, id).into()
  348. }
  349. // Parse recipient wallet address
  350. let Some(recipient) = params.get("recipient") else {
  351. return server_error(RpcError::MinerMissingRecipient, id, None)
  352. };
  353. let Some(recipient_str) = recipient.get::<String>() else {
  354. return server_error(RpcError::MinerInvalidRecipient, id, None)
  355. };
  356. let Ok(recipient) = Address::from_str(recipient_str) else {
  357. return server_error(RpcError::MinerInvalidRecipient, id, None)
  358. };
  359. if recipient.network() != self.network {
  360. return server_error(RpcError::MinerInvalidRecipientPrefix, id, None)
  361. };
  362. // Parse spend hook
  363. let spend_hook = match params.get("spend_hook") {
  364. Some(spend_hook) => {
  365. let Some(spend_hook) = spend_hook.get::<String>() else {
  366. return server_error(RpcError::MinerInvalidSpendHook, id, None)
  367. };
  368. let Ok(spend_hook) = FuncId::from_str(spend_hook) else {
  369. return server_error(RpcError::MinerInvalidSpendHook, id, None)
  370. };
  371. Some(spend_hook)
  372. }
  373. None => None,
  374. };
  375. // Parse user data
  376. let user_data: Option<pallas::Base> = match params.get("user_data") {
  377. Some(user_data) => {
  378. let Some(user_data) = user_data.get::<String>() else {
  379. return server_error(RpcError::MinerInvalidUserData, id, None)
  380. };
  381. let Ok(bytes) = bs58::decode(&user_data).into_vec() else {
  382. return server_error(RpcError::MinerInvalidUserData, id, None)
  383. };
  384. let bytes: [u8; 32] = match bytes.try_into() {
  385. Ok(b) => b,
  386. Err(_) => return server_error(RpcError::MinerInvalidUserData, id, None),
  387. };
  388. let Some(user_data) = pallas::Base::from_repr(bytes).into() else {
  389. return server_error(RpcError::MinerInvalidUserData, id, None)
  390. };
  391. Some(user_data)
  392. }
  393. None => None,
  394. };
  395. // Parse nonce
  396. let Some(nonce) = params.get("nonce") else {
  397. return server_error(RpcError::MinerMissingNonce, id, None)
  398. };
  399. let Some(nonce) = nonce.get::<f64>() else {
  400. return server_error(RpcError::MinerInvalidNonce, id, None)
  401. };
  402. // If we don't know about this job, we can just abort here.
  403. let address_bytes =
  404. serialize_async(&(recipient_str.clone().into_bytes(), spend_hook, user_data)).await;
  405. let mut blocktemplates = self.blocktemplates.lock().await;
  406. let Some(blocktemplate) = blocktemplates.get(&address_bytes) else {
  407. return server_error(RpcError::MinerUnknownJob, id, None)
  408. };
  409. info!(
  410. target: "darkfid::rpc::miner_submit_solution",
  411. "[RPC] Got solution submission for block template: {}", blocktemplate.block.header.hash(),
  412. );
  413. // Sign the DarkFi block
  414. let mut block = blocktemplate.block.clone();
  415. block.header.nonce = *nonce as u32;
  416. block.sign(&blocktemplate.secret);
  417. info!(
  418. target: "darkfid::rpc::miner_submit_solution",
  419. "[RPC] Mined block header hash: {}", blocktemplate.block.header.hash(),
  420. );
  421. // At this point we should be able to remove the submitted job.
  422. // We still won't release the lock in hope of proposing the block
  423. // first.
  424. blocktemplates.remove(&address_bytes);
  425. // Propose the new block
  426. info!(
  427. target: "darkfid::rpc::miner_submit_solution",
  428. "[RPC] Proposing new block to network",
  429. );
  430. let proposal = Proposal::new(block);
  431. if let Err(e) = self.validator.append_proposal(&proposal).await {
  432. error!(
  433. target: "darkfid::rpc::miner_submit_solution",
  434. "[RPC] Error proposing new block: {e}",
  435. );
  436. return JsonResponse::new(JsonValue::String(String::from("rejected")), id).into()
  437. }
  438. let proposals_sub = self.subscribers.get("proposals").unwrap();
  439. let enc_prop = JsonValue::String(base64::encode(&serialize_async(&proposal).await));
  440. proposals_sub.notify(vec![enc_prop].into()).await;
  441. info!(
  442. target: "darkfid::rpc::miner_submit_solution",
  443. "[RPC] Broadcasting new block to network",
  444. );
  445. let message = ProposalMessage(proposal);
  446. self.p2p_handler.p2p.broadcast(&message).await;
  447. JsonResponse::new(JsonValue::String(String::from("accepted")), id).into()
  448. }
  449. }
  450. /// Auxiliary function to generate next block in an atomic manner.
  451. pub async fn generate_next_block(
  452. extended_fork: &mut Fork,
  453. recipient_config: &MinerRewardsRecipientConfig,
  454. zkbin: &ZkBinary,
  455. pk: &ProvingKey,
  456. block_target: u32,
  457. verify_fees: bool,
  458. ) -> Result<(BigUint, BlockInfo, SecretKey)> {
  459. // Grab forks' last block proposal(previous)
  460. let last_proposal = extended_fork.last_proposal()?;
  461. // Grab forks' next block height
  462. let next_block_height = last_proposal.block.header.height + 1;
  463. // Grab forks' unproposed transactions
  464. let (mut txs, _, fees, overlay) = extended_fork
  465. .unproposed_txs(&extended_fork.blockchain, next_block_height, block_target, verify_fees)
  466. .await?;
  467. // Create an ephemeral block signing keypair. Its secret key will
  468. // be stored in the PowReward transaction's encrypted note for
  469. // later retrieval. It is encrypted towards the recipient's public
  470. // key.
  471. let block_signing_keypair = Keypair::random(&mut OsRng);
  472. // Generate reward transaction
  473. let tx = generate_transaction(
  474. next_block_height,
  475. fees,
  476. &block_signing_keypair,
  477. recipient_config,
  478. zkbin,
  479. pk,
  480. )?;
  481. // Apply producer transaction in the overlay
  482. let _ = apply_producer_transaction(
  483. &overlay,
  484. next_block_height,
  485. block_target,
  486. &tx,
  487. &mut MerkleTree::new(1),
  488. )
  489. .await?;
  490. txs.push(tx);
  491. // Grab the updated contracts states root
  492. let diff = overlay.lock().unwrap().overlay.lock().unwrap().diff(&extended_fork.diffs)?;
  493. overlay
  494. .lock()
  495. .unwrap()
  496. .contracts
  497. .update_state_monotree(&diff, &mut extended_fork.state_monotree)?;
  498. let Some(state_root) = extended_fork.state_monotree.get_headroot()? else {
  499. return Err(Error::ContractsStatesRootNotFoundError);
  500. };
  501. // Drop new trees opened by the unproposed transactions overlay
  502. overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
  503. // Generate the new header
  504. let mut header =
  505. Header::new(last_proposal.hash, next_block_height, Timestamp::current_time(), 0);
  506. header.state_root = state_root;
  507. // Generate the block
  508. let mut next_block = BlockInfo::new_empty(header);
  509. // Add transactions to the block
  510. next_block.append_txs(txs);
  511. // Grab the next mine target
  512. let target = extended_fork.module.next_mine_target()?;
  513. Ok((target, next_block, block_signing_keypair.secret))
  514. }
  515. /// Auxiliary function to generate a Money::PoWReward transaction.
  516. fn generate_transaction(
  517. block_height: u32,
  518. fees: u64,
  519. block_signing_keypair: &Keypair,
  520. recipient_config: &MinerRewardsRecipientConfig,
  521. zkbin: &ZkBinary,
  522. pk: &ProvingKey,
  523. ) -> Result<Transaction> {
  524. // Build the transaction debris
  525. let debris = PoWRewardCallBuilder {
  526. signature_keypair: *block_signing_keypair,
  527. block_height,
  528. fees,
  529. recipient: Some(*recipient_config.recipient.public_key()),
  530. spend_hook: recipient_config.spend_hook,
  531. user_data: recipient_config.user_data,
  532. mint_zkbin: zkbin.clone(),
  533. mint_pk: pk.clone(),
  534. }
  535. .build()?;
  536. // Generate and sign the actual transaction
  537. let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
  538. debris.params.encode(&mut data)?;
  539. let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
  540. let mut tx_builder =
  541. TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
  542. let mut tx = tx_builder.build()?;
  543. let sigs = tx.create_sigs(&[block_signing_keypair.secret])?;
  544. tx.signatures = vec![sigs];
  545. Ok(tx)
  546. }