1use chia_bls::Signature;
2use chia_protocol::{Bytes32, Coin, CoinSpend};
3use chia_puzzle_types::singleton::{LauncherSolution, SingletonArgs};
4use chia_puzzle_types::{
5 LineageProof, Proof,
6 singleton::{SingletonSolution, SingletonStruct},
7};
8use chia_sdk_types::puzzles::{
9 RawActionLayerSolution, ReserveFinalizerSolution, RewardDistributorCommitmentSlotValue,
10 RewardDistributorEntrySlotValue, RewardDistributorRewardSlotValue, RewardDistributorSlotNonce,
11 SlotInfo,
12};
13use chia_sdk_types::{Condition, Conditions};
14use clvm_traits::{FromClvm, clvm_list, match_tuple};
15use clvm_utils::{ToTreeHash, tree_hash};
16use clvmr::NodePtr;
17
18use crate::{
19 ActionLayer, ActionLayerSolution, ActionSingleton, Cat, CatSpend, DriverError, Layer, Puzzle,
20 RewardDistributorAddEntryAction, RewardDistributorAddIncentivesAction,
21 RewardDistributorCommitIncentivesAction, RewardDistributorInitiatePayoutAction,
22 RewardDistributorNewEpochAction, RewardDistributorRemoveEntryAction,
23 RewardDistributorStakeAction, RewardDistributorSyncAction, RewardDistributorUnstakeAction,
24 RewardDistributorWithdrawIncentivesAction, SingletonAction, SingletonLayer, Slot, Spend,
25 SpendContext,
26};
27
28use super::{Reserve, RewardDistributorConstants, RewardDistributorInfo, RewardDistributorState};
29
30#[derive(Debug, Clone)]
31pub struct RewardDistributorPendingSpendInfo {
32 pub actions: Vec<Spend>,
33
34 pub spent_reward_slots: Vec<RewardDistributorRewardSlotValue>,
35 pub spent_commitment_slots: Vec<RewardDistributorCommitmentSlotValue>,
36 pub spent_entry_slots: Vec<RewardDistributorEntrySlotValue>,
37
38 pub created_reward_slots: Vec<RewardDistributorRewardSlotValue>,
39 pub created_commitment_slots: Vec<RewardDistributorCommitmentSlotValue>,
40 pub created_entry_slots: Vec<RewardDistributorEntrySlotValue>,
41
42 pub latest_state: (NodePtr, RewardDistributorState),
43
44 pub signature: Signature,
45 pub other_cats: Vec<CatSpend>,
46}
47
48impl RewardDistributorPendingSpendInfo {
49 pub fn new(latest_state: RewardDistributorState) -> Self {
50 Self {
51 actions: vec![],
52 created_reward_slots: vec![],
53 created_commitment_slots: vec![],
54 created_entry_slots: vec![],
55 spent_reward_slots: vec![],
56 spent_commitment_slots: vec![],
57 spent_entry_slots: vec![],
58 latest_state: (NodePtr::NIL, latest_state),
59 signature: Signature::default(),
60 other_cats: vec![],
61 }
62 }
63
64 pub fn add_delta(&mut self, delta: RewardDistributorPendingSpendInfo) {
65 self.actions.extend(delta.actions);
66
67 self.spent_reward_slots.extend(delta.spent_reward_slots);
68 self.spent_commitment_slots
69 .extend(delta.spent_commitment_slots);
70 self.spent_entry_slots.extend(delta.spent_entry_slots);
71
72 self.created_reward_slots.extend(delta.created_reward_slots);
73 self.created_commitment_slots
74 .extend(delta.created_commitment_slots);
75 self.created_entry_slots.extend(delta.created_entry_slots);
76
77 self.latest_state = delta.latest_state;
78
79 }
82}
83
84#[derive(Debug, Clone)]
85#[must_use]
86pub struct RewardDistributor {
87 pub coin: Coin,
88 pub proof: Proof,
89 pub info: RewardDistributorInfo,
90 pub reserve: Reserve,
91
92 pub pending_spend: RewardDistributorPendingSpendInfo,
93}
94
95impl RewardDistributor {
96 pub fn new(coin: Coin, proof: Proof, info: RewardDistributorInfo, reserve: Reserve) -> Self {
97 Self {
98 coin,
99 proof,
100 info,
101 reserve,
102 pending_spend: RewardDistributorPendingSpendInfo::new(info.state),
103 }
104 }
105}
106
107impl RewardDistributor {
108 #[allow(clippy::type_complexity)]
109 pub fn pending_info_delta_from_spend(
110 ctx: &mut SpendContext,
111 action_spend: Spend,
112 current_state_and_ephemeral: (NodePtr, RewardDistributorState),
113 constants: RewardDistributorConstants,
114 ) -> Result<RewardDistributorPendingSpendInfo, DriverError> {
115 let mut spent_reward_slots = vec![];
116 let mut spent_commitment_slots = vec![];
117 let mut spent_entry_slots = vec![];
118
119 let mut created_reward_slots = vec![];
120 let mut created_commitment_slots = vec![];
121 let mut created_entry_slots = vec![];
122
123 let new_epoch_action = RewardDistributorNewEpochAction::from_constants(&constants);
124 let new_epoch_hash = new_epoch_action.tree_hash();
125
126 let commit_incentives_action =
127 RewardDistributorCommitIncentivesAction::from_constants(&constants);
128 let commit_incentives_hash = commit_incentives_action.tree_hash();
129
130 let add_entry_action = RewardDistributorAddEntryAction::from_constants(&constants);
131 let add_entry_hash = add_entry_action.tree_hash();
132
133 let remove_entry_action = RewardDistributorRemoveEntryAction::from_constants(&constants);
134 let remove_entry_hash = remove_entry_action.tree_hash();
135
136 let stake_action = RewardDistributorStakeAction::from_constants(&constants);
137 let stake_hash = stake_action.tree_hash();
138
139 let unstake_action = RewardDistributorUnstakeAction::from_constants(&constants);
140 let unstake_hash = unstake_action.tree_hash();
141
142 let withdraw_incentives_action =
143 RewardDistributorWithdrawIncentivesAction::from_constants(&constants);
144 let withdraw_incentives_hash = withdraw_incentives_action.tree_hash();
145
146 let initiate_payout_action =
147 RewardDistributorInitiatePayoutAction::from_constants(&constants);
148 let initiate_payout_hash = initiate_payout_action.tree_hash();
149
150 let add_incentives_action =
151 RewardDistributorAddIncentivesAction::from_constants(&constants);
152 let add_incentives_hash = add_incentives_action.tree_hash();
153
154 let sync_action = RewardDistributorSyncAction::from_constants(&constants);
155 let sync_hash = sync_action.tree_hash();
156
157 let actual_solution = ctx.alloc(&clvm_list!(
158 current_state_and_ephemeral,
159 action_spend.solution
160 ))?;
161
162 let output = ctx.run(action_spend.puzzle, actual_solution)?;
163 let (new_state_and_ephemeral, _) =
164 ctx.extract::<match_tuple!((NodePtr, RewardDistributorState), NodePtr)>(output)?;
165
166 let raw_action_hash = ctx.tree_hash(action_spend.puzzle);
167
168 if raw_action_hash == new_epoch_hash {
169 created_reward_slots.push(RewardDistributorNewEpochAction::created_slot_value(
170 ctx,
171 action_spend.solution,
172 )?);
173 spent_reward_slots.push(RewardDistributorNewEpochAction::spent_slot_value(
174 ctx,
175 action_spend.solution,
176 )?);
177 } else if raw_action_hash == commit_incentives_hash {
178 let (comm, rews) = RewardDistributorCommitIncentivesAction::created_slot_values(
179 ctx,
180 constants.epoch_seconds,
181 action_spend.solution,
182 )?;
183
184 created_commitment_slots.push(comm);
185 created_reward_slots.extend(rews);
186 spent_reward_slots.push(RewardDistributorCommitIncentivesAction::spent_slot_value(
187 ctx,
188 action_spend.solution,
189 )?);
190 } else if raw_action_hash == add_entry_hash {
191 created_entry_slots.push(RewardDistributorAddEntryAction::created_slot_value(
192 ctx,
193 ¤t_state_and_ephemeral.1,
194 action_spend.solution,
195 )?);
196 } else if raw_action_hash == stake_hash {
197 created_entry_slots.push(RewardDistributorStakeAction::created_slot_value(
198 ctx,
199 ¤t_state_and_ephemeral.1,
200 action_spend.solution,
201 )?);
202 } else if raw_action_hash == remove_entry_hash {
203 spent_entry_slots.push(RewardDistributorRemoveEntryAction::spent_slot_value(
204 ctx,
205 action_spend.solution,
206 )?);
207 } else if raw_action_hash == unstake_hash {
208 spent_entry_slots.push(RewardDistributorUnstakeAction::spent_slot_value(
209 ctx,
210 action_spend.solution,
211 )?);
212 } else if raw_action_hash == withdraw_incentives_hash {
213 let (rew, cmt) = RewardDistributorWithdrawIncentivesAction::spent_slot_values(
214 ctx,
215 action_spend.solution,
216 )?;
217
218 spent_reward_slots.push(rew);
219 spent_commitment_slots.push(cmt);
220 created_reward_slots.push(
221 RewardDistributorWithdrawIncentivesAction::created_slot_value(
222 ctx,
223 constants.withdrawal_share_bps,
224 action_spend.solution,
225 )?,
226 );
227 } else if raw_action_hash == initiate_payout_hash {
228 created_entry_slots.push(RewardDistributorInitiatePayoutAction::created_slot_value(
229 ctx,
230 ¤t_state_and_ephemeral.1,
231 action_spend.solution,
232 )?);
233 spent_entry_slots.push(RewardDistributorInitiatePayoutAction::spent_slot_value(
234 ctx,
235 action_spend.solution,
236 )?);
237 } else if raw_action_hash != add_incentives_hash && raw_action_hash != sync_hash {
238 return Err(DriverError::InvalidMerkleProof);
240 }
241
242 Ok(RewardDistributorPendingSpendInfo {
243 actions: vec![action_spend],
244 spent_reward_slots,
245 spent_commitment_slots,
246 spent_entry_slots,
247 created_reward_slots,
248 created_commitment_slots,
249 created_entry_slots,
250 latest_state: new_state_and_ephemeral,
251 signature: Signature::default(),
252 other_cats: vec![],
253 })
254 }
255
256 pub fn pending_info_from_spend(
257 ctx: &mut SpendContext,
258 inner_solution: NodePtr,
259 initial_state: RewardDistributorState,
260 constants: RewardDistributorConstants,
261 ) -> Result<RewardDistributorPendingSpendInfo, DriverError> {
262 let mut pending_spend_info = RewardDistributorPendingSpendInfo::new(initial_state);
263
264 let inner_solution =
265 ActionLayer::<RewardDistributorState, NodePtr>::parse_solution(ctx, inner_solution)?;
266
267 for raw_action in &inner_solution.action_spends {
268 let delta = Self::pending_info_delta_from_spend(
269 ctx,
270 *raw_action,
271 pending_spend_info.latest_state,
272 constants,
273 )?;
274
275 pending_spend_info.add_delta(delta);
276 }
277
278 Ok(pending_spend_info)
279 }
280
281 pub fn from_spend(
282 ctx: &mut SpendContext,
283 spend: &CoinSpend,
284 reserve_lineage_proof: Option<LineageProof>,
285 constants: RewardDistributorConstants,
286 ) -> Result<Option<Self>, DriverError> {
287 let coin = spend.coin;
288 let puzzle_ptr = ctx.alloc(&spend.puzzle_reveal)?;
289 let puzzle = Puzzle::parse(ctx, puzzle_ptr);
290 let solution_ptr = ctx.alloc(&spend.solution)?;
291
292 let Some(info) = RewardDistributorInfo::parse(ctx, puzzle, constants)? else {
293 return Ok(None);
294 };
295
296 let solution = ctx.extract::<SingletonSolution<NodePtr>>(solution_ptr)?;
297 let proof = solution.lineage_proof;
298
299 let pending_spend =
300 Self::pending_info_from_spend(ctx, solution.inner_solution, info.state, constants)?;
301
302 let inner_solution =
303 RawActionLayerSolution::<NodePtr, NodePtr, ReserveFinalizerSolution>::from_clvm(
304 ctx,
305 solution.inner_solution,
306 )?;
307
308 let reserve = Reserve::new(
309 inner_solution.finalizer_solution.reserve_parent_id,
310 reserve_lineage_proof.unwrap_or(LineageProof {
311 parent_parent_coin_info: Bytes32::default(),
312 parent_inner_puzzle_hash: Bytes32::default(),
313 parent_amount: 0,
314 }), constants.reserve_asset_id,
316 SingletonStruct::new(info.constants.launcher_id)
317 .tree_hash()
318 .into(),
319 0,
320 info.state.total_reserves,
321 );
322
323 Ok(Some(RewardDistributor {
324 coin,
325 proof,
326 info,
327 reserve,
328 pending_spend,
329 }))
330 }
331
332 pub fn child_lineage_proof(&self) -> LineageProof {
333 LineageProof {
334 parent_parent_coin_info: self.coin.parent_coin_info,
335 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
336 parent_amount: self.coin.amount,
337 }
338 }
339
340 pub fn from_parent_spend(
341 ctx: &mut SpendContext,
342 parent_spend: &CoinSpend,
343 constants: RewardDistributorConstants,
344 ) -> Result<Option<Self>, DriverError>
345 where
346 Self: Sized,
347 {
348 let Some(parent_registry) = Self::from_spend(ctx, parent_spend, None, constants)? else {
349 return Ok(None);
350 };
351
352 let new_info = parent_registry
353 .info
354 .with_state(parent_registry.pending_spend.latest_state.1);
355
356 Ok(Some(RewardDistributor {
357 coin: Coin::new(
358 parent_registry.coin.coin_id(),
359 new_info.puzzle_hash().into(),
360 1,
361 ),
362 proof: Proof::Lineage(parent_registry.child_lineage_proof()),
363 info: new_info,
364 reserve: parent_registry.reserve.child(new_info.state.total_reserves),
365 pending_spend: RewardDistributorPendingSpendInfo::new(new_info.state),
366 }))
367 }
368
369 pub fn child(&self, child_state: RewardDistributorState) -> Self {
370 let new_info = self.info.with_state(child_state);
371 let new_coin = Coin::new(self.coin.coin_id(), new_info.puzzle_hash().into(), 1);
372 let new_reserve = self.reserve.child(child_state.total_reserves);
373
374 RewardDistributor {
375 coin: new_coin,
376 proof: Proof::Lineage(self.child_lineage_proof()),
377 info: new_info,
378 reserve: new_reserve,
379 pending_spend: RewardDistributorPendingSpendInfo::new(new_info.state),
380 }
381 }
382
383 #[allow(clippy::type_complexity)]
384 pub fn from_launcher_solution(
385 ctx: &mut SpendContext,
386 launcher_coin: Coin,
387 launcher_solution: NodePtr,
388 ) -> Result<Option<(RewardDistributorConstants, RewardDistributorState, Coin)>, DriverError>
389 where
390 Self: Sized,
391 {
392 let Ok(launcher_solution) =
393 ctx.extract::<LauncherSolution<(u64, RewardDistributorConstants)>>(launcher_solution)
394 else {
395 return Ok(None);
396 };
397
398 let launcher_id = launcher_coin.coin_id();
399 let (first_epoch_start, constants) = launcher_solution.key_value_list;
400
401 if constants != constants.with_launcher_id(launcher_id) {
402 return Err(DriverError::Custom(
403 "Distributor constants invalid".to_string(),
404 ));
405 }
406
407 let distributor_eve_coin =
408 Coin::new(launcher_id, launcher_solution.singleton_puzzle_hash, 1);
409
410 let initial_state = RewardDistributorState::initial(first_epoch_start);
411
412 Ok(Some((constants, initial_state, distributor_eve_coin)))
413 }
414
415 #[allow(clippy::type_complexity)]
416 pub fn from_eve_coin_spend(
417 ctx: &mut SpendContext,
418 constants: RewardDistributorConstants,
419 initial_state: RewardDistributorState,
420 eve_coin_spend: &CoinSpend,
421 reserve_parent_id: Bytes32,
422 reserve_lineage_proof: LineageProof,
423 ) -> Result<Option<(RewardDistributor, Slot<RewardDistributorRewardSlotValue>)>, DriverError>
424 where
425 Self: Sized,
426 {
427 let eve_coin_puzzle_ptr = ctx.alloc(&eve_coin_spend.puzzle_reveal)?;
428 let eve_coin_puzzle = Puzzle::parse(ctx, eve_coin_puzzle_ptr);
429 let Some(eve_coin_puzzle) = SingletonLayer::<NodePtr>::parse_puzzle(ctx, eve_coin_puzzle)?
430 else {
431 return Err(DriverError::Custom("Eve coin not a singleton".to_string()));
432 };
433
434 let eve_coin_inner_puzzle_hash = tree_hash(ctx, eve_coin_puzzle.inner_puzzle);
435
436 let eve_coin_solution_ptr = ctx.alloc(&eve_coin_spend.solution)?;
437 let eve_coin_output = ctx.run(eve_coin_puzzle_ptr, eve_coin_solution_ptr)?;
438 let eve_coin_output = ctx.extract::<Conditions<NodePtr>>(eve_coin_output)?;
439
440 let Some(Condition::CreateCoin(odd_create_coin)) = eve_coin_output.into_iter().find(|c| {
441 if let Condition::CreateCoin(create_coin) = c {
442 create_coin.amount % 2 == 1
444 } else {
445 false
446 }
447 }) else {
448 return Err(DriverError::Custom(
449 "Eve coin did not create a coin".to_string(),
450 ));
451 };
452
453 let new_coin = Coin::new(
454 eve_coin_spend.coin.coin_id(),
455 odd_create_coin.puzzle_hash,
456 odd_create_coin.amount,
457 );
458 let lineage_proof = LineageProof {
459 parent_parent_coin_info: eve_coin_spend.coin.parent_coin_info,
460 parent_inner_puzzle_hash: eve_coin_inner_puzzle_hash.into(),
461 parent_amount: eve_coin_spend.coin.amount,
462 };
463 let reserve = Reserve::new(
464 reserve_parent_id,
465 reserve_lineage_proof,
466 constants.reserve_asset_id,
467 SingletonStruct::new(constants.launcher_id)
468 .tree_hash()
469 .into(),
470 0,
471 0,
472 );
473 let new_distributor = RewardDistributor::new(
474 new_coin,
475 Proof::Lineage(lineage_proof),
476 RewardDistributorInfo::new(initial_state, constants),
477 reserve,
478 );
479
480 if SingletonArgs::curry_tree_hash(
481 constants.launcher_id,
482 new_distributor.info.inner_puzzle_hash(),
483 ) != new_distributor.coin.puzzle_hash.into()
484 {
485 return Err(DriverError::Custom(
486 "Distributor singleton puzzle hash mismatch".to_string(),
487 ));
488 }
489
490 let slot_value = RewardDistributorRewardSlotValue {
491 epoch_start: initial_state.round_time_info.epoch_end,
492 next_epoch_initialized: false,
493 rewards: 0,
494 };
495
496 let slot = Slot::new(
497 lineage_proof,
498 SlotInfo::from_value(
499 constants.launcher_id,
500 RewardDistributorSlotNonce::REWARD.to_u64(),
501 slot_value,
502 ),
503 );
504
505 Ok(Some((new_distributor, slot)))
506 }
507
508 pub fn set_pending_signature(&mut self, signature: Signature) {
509 self.pending_spend.signature = signature;
510 }
511
512 pub fn set_pending_other_cats(&mut self, other_cats: Vec<CatSpend>) {
513 self.pending_spend.other_cats = other_cats;
514 }
515}
516
517impl ActionSingleton for RewardDistributor {
518 type State = RewardDistributorState;
519 type Constants = RewardDistributorConstants;
520}
521
522impl RewardDistributor {
523 pub fn finish_spend(
524 self,
525 ctx: &mut SpendContext,
526 other_cat_spends: Vec<CatSpend>,
527 ) -> Result<(Self, Signature), DriverError> {
528 let layers = self.info.into_layers(ctx)?;
529
530 let puzzle = layers.construct_puzzle(ctx)?;
531
532 let action_puzzle_hashes = self
533 .pending_spend
534 .actions
535 .iter()
536 .map(|a| ctx.tree_hash(a.puzzle).into())
537 .collect::<Vec<Bytes32>>();
538
539 let finalizer_solution = ctx.alloc(&ReserveFinalizerSolution {
540 reserve_parent_id: self.reserve.coin.parent_coin_info,
541 })?;
542
543 let child = self.child(self.pending_spend.latest_state.1);
544 let solution = layers.construct_solution(
545 ctx,
546 SingletonSolution {
547 lineage_proof: self.proof,
548 amount: self.coin.amount,
549 inner_solution: ActionLayerSolution {
550 proofs: layers
551 .inner_puzzle
552 .get_proofs(
553 &RewardDistributorInfo::action_puzzle_hashes(&self.info.constants),
554 &action_puzzle_hashes,
555 )
556 .ok_or(DriverError::Custom(
557 "Couldn't build proofs for one or more actions".to_string(),
558 ))?,
559 action_spends: self.pending_spend.actions,
560 finalizer_solution,
561 },
562 },
563 )?;
564
565 let my_spend = Spend::new(puzzle, solution);
566 ctx.spend(self.coin, my_spend)?;
567
568 let cat_spend = self.reserve.cat_spend_for_reserve_finalizer_controller(
569 ctx,
570 self.info.state,
571 self.info.inner_puzzle_hash().into(),
572 solution,
573 )?;
574
575 let mut cat_spends = other_cat_spends;
576 cat_spends.push(cat_spend);
577 cat_spends.extend(self.pending_spend.other_cats);
578 Cat::spend_all(ctx, &cat_spends)?;
579
580 Ok((child, self.pending_spend.signature))
581 }
582
583 pub fn new_action<A>(&self) -> A
584 where
585 A: SingletonAction<Self>,
586 {
587 A::from_constants(&self.info.constants)
588 }
589
590 pub fn created_slot_value_to_slot<SlotValue>(
591 &self,
592 slot_value: SlotValue,
593 nonce: RewardDistributorSlotNonce,
594 ) -> Slot<SlotValue>
595 where
596 SlotValue: Copy + ToTreeHash,
597 {
598 Slot::new(
599 LineageProof {
600 parent_parent_coin_info: self.coin.parent_coin_info,
601 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
602 parent_amount: self.coin.amount,
603 },
604 SlotInfo::from_value(self.info.constants.launcher_id, nonce.to_u64(), slot_value),
605 )
606 }
607
608 pub fn insert_action_spend(
609 &mut self,
610 ctx: &mut SpendContext,
611 action_spend: Spend,
612 ) -> Result<(), DriverError> {
613 let delta = Self::pending_info_delta_from_spend(
614 ctx,
615 action_spend,
616 self.pending_spend.latest_state,
617 self.info.constants,
618 )?;
619
620 self.pending_spend.add_delta(delta);
621
622 Ok(())
623 }
624
625 pub fn actual_reward_slot_value(
626 &self,
627 slot: Slot<RewardDistributorRewardSlotValue>,
628 ) -> Slot<RewardDistributorRewardSlotValue> {
629 let mut slot = slot;
630
631 for slot_value in &self.pending_spend.created_reward_slots {
632 if slot_value.epoch_start == slot.info.value.epoch_start {
633 slot = self
634 .created_slot_value_to_slot(*slot_value, RewardDistributorSlotNonce::REWARD);
635 }
636 }
637
638 slot
639 }
640
641 pub fn actual_entry_slot_value(
642 &self,
643 slot: Slot<RewardDistributorEntrySlotValue>,
644 ) -> Slot<RewardDistributorEntrySlotValue> {
645 let mut slot = slot;
646
647 for slot_value in &self.pending_spend.created_entry_slots {
648 if slot_value.payout_puzzle_hash == slot.info.value.payout_puzzle_hash {
649 slot =
650 self.created_slot_value_to_slot(*slot_value, RewardDistributorSlotNonce::ENTRY);
651 }
652 }
653
654 slot
655 }
656
657 pub fn actual_commitment_slot_value(
658 &self,
659 slot: Slot<RewardDistributorCommitmentSlotValue>,
660 ) -> Slot<RewardDistributorCommitmentSlotValue> {
661 let mut slot = slot;
662
663 for slot_value in &self.pending_spend.created_commitment_slots {
664 if slot_value.epoch_start == slot.info.value.epoch_start {
665 slot = self.created_slot_value_to_slot(
666 *slot_value,
667 RewardDistributorSlotNonce::COMMITMENT,
668 );
669 }
670 }
671
672 slot
673 }
674}