Skip to content

Commit

Permalink
add support for stake pool creation from trezor and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
OBorce committed Feb 26, 2025
1 parent c9c1c91 commit 36c9a71
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 128 deletions.
22 changes: 14 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

185 changes: 112 additions & 73 deletions wallet/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,10 @@ impl<K: AccountKeyChains> Account<K> {
}
}

let selected_inputs = selected_inputs.into_iter().flat_map(|x| x.1.into_output_pairs());
let selected_inputs = selected_inputs
.into_iter()
.flat_map(|x| x.1.into_output_pairs())
.map(|(inp, out)| (inp, out, None));

let pool_data_getter = |pool_id: &PoolId| self.output_cache.pool_data(*pool_id).ok();
request.with_inputs(selected_inputs, &pool_data_getter)
Expand Down Expand Up @@ -728,7 +731,7 @@ impl<K: AccountKeyChains> Account<K> {
.ok_or(WalletError::NoUtxos)?;

let mut req = SendRequest::new()
.with_inputs([(tx_input, input_utxo.clone())], &|id| {
.with_inputs([(tx_input, input_utxo.clone(), None)], &|id| {
(*id == pool_id).then_some(pool_data)
})?
.with_outputs([output]);
Expand Down Expand Up @@ -1324,6 +1327,107 @@ impl<K: AccountKeyChains> Account<K> {
)
}

pub fn create_stake_pool_tx_with_vrf_key(
&mut self,
db_tx: &mut impl WalletStorageWriteUnlocked,
mut stake_pool_arguments: StakePoolCreationArguments,
median_time: BlockTimestamp,
fee_rate: CurrentFeeRate,
) -> WalletResult<SendRequest> {
let Some(vrf_public_key) = stake_pool_arguments.vrf_public_key.take() else {
return Err(WalletError::VrfKeyMustBeProvided);
};

self.create_stake_pool_tx_impl(
stake_pool_arguments,
db_tx,
vrf_public_key,
median_time,
fee_rate,
)
}

fn create_stake_pool_tx_impl(
&mut self,
stake_pool_arguments: StakePoolCreationArguments,
db_tx: &mut impl WalletStorageWriteUnlocked,
vrf_public_key: VRFPublicKey,
median_time: BlockTimestamp,
fee_rate: CurrentFeeRate,
) -> Result<SendRequest, WalletError> {
let staker = match stake_pool_arguments.staker_key {
Some(staker) => match staker {
Destination::PublicKey(_) => staker,
// Note: technically it's possible to create a pool with PublicKeyHash as the staker,
// the pool will seem to work and will actually try producing blocks. However,
// the produced blocks will be rejected by chainstate, see get_staking_kernel_destination
// in `consensus`.
Destination::AnyoneCanSpend
| Destination::PublicKeyHash(_)
| Destination::ScriptHash(_)
| Destination::ClassicMultisig(_) => {
return Err(WalletError::StakerDestinationMustBePublicKey)
}
},
None => Destination::PublicKey(
self.key_chain.issue_key(db_tx, KeyPurpose::ReceiveFunds)?.into_public_key(),
),
};

// the first UTXO is needed in advance to calculate pool_id, so just make a dummy one
// and then replace it with when we can calculate the pool_id
let dummy_pool_id = PoolId::new(Uint256::from_u64(0).into());
let dummy_stake_output = make_stake_output(
dummy_pool_id,
StakePoolCreationResolvedArguments {
amount: stake_pool_arguments.amount,
margin_ratio_per_thousand: stake_pool_arguments.margin_ratio_per_thousand,
cost_per_block: stake_pool_arguments.cost_per_block,
decommission_key: stake_pool_arguments.decommission_key,
staker_key: staker,
vrf_public_key,
},
);
let request = SendRequest::new().with_outputs([dummy_stake_output]);
let mut request = self.select_inputs_for_send_request(
request,
SelectedInputs::Utxos(vec![]),
None,
BTreeMap::new(),
db_tx,
median_time,
fee_rate,
None,
)?;

let input0_outpoint = crate::utils::get_first_utxo_outpoint(request.inputs())?;
let new_pool_id = pos_accounting::make_pool_id(input0_outpoint);

// update the dummy_pool_id with the new pool_id
let old_pool_id = request
.get_outputs_mut()
.iter_mut()
.find_map(|out| match out {
TxOutput::CreateStakePool(pool_id, _) if *pool_id == dummy_pool_id => Some(pool_id),
TxOutput::CreateStakePool(_, _)
| TxOutput::Burn(_)
| TxOutput::Transfer(_, _)
| TxOutput::DelegateStaking(_, _)
| TxOutput::LockThenTransfer(_, _, _)
| TxOutput::CreateDelegationId(_, _)
| TxOutput::ProduceBlockFromStake(_, _)
| TxOutput::IssueFungibleToken(_)
| TxOutput::IssueNft(_, _, _)
| TxOutput::DataDeposit(_)
| TxOutput::Htlc(_, _)
| TxOutput::CreateOrder(_) => None,
})
.expect("find output with dummy_pool_id");
*old_pool_id = new_pool_id;

Ok(request)
}

pub fn pool_exists(&self, pool_id: PoolId) -> bool {
self.output_cache.pool_data(pool_id).is_ok()
}
Expand Down Expand Up @@ -2200,86 +2304,21 @@ impl<K: AccountKeyChains + VRFAccountKeyChains> Account<K> {
pub fn create_stake_pool_tx(
&mut self,
db_tx: &mut impl WalletStorageWriteUnlocked,
stake_pool_arguments: StakePoolCreationArguments,
mut stake_pool_arguments: StakePoolCreationArguments,
median_time: BlockTimestamp,
fee_rate: CurrentFeeRate,
) -> WalletResult<SendRequest> {
// TODO: Use other accounts here
let staker = match stake_pool_arguments.staker_key {
Some(staker) => match staker {
Destination::PublicKey(_) => staker,
// Note: technically it's possible to create a pool with PublicKeyHash as the staker,
// the pool will seem to work and will actually try producing blocks. However,
// the produced blocks will be rejected by chainstate, see get_staking_kernel_destination
// in `consensus`.
Destination::AnyoneCanSpend
| Destination::PublicKeyHash(_)
| Destination::ScriptHash(_)
| Destination::ClassicMultisig(_) => {
return Err(WalletError::StakerDestinationMustBePublicKey)
}
},
None => Destination::PublicKey(
self.key_chain.issue_key(db_tx, KeyPurpose::ReceiveFunds)?.into_public_key(),
),
};
let vrf_public_key = match stake_pool_arguments.vrf_public_key {
let vrf_public_key = match stake_pool_arguments.vrf_public_key.take() {
Some(vrf_public_key) => vrf_public_key,
None => self.get_vrf_public_key(db_tx)?,
};

// the first UTXO is needed in advance to calculate pool_id, so just make a dummy one
// and then replace it with when we can calculate the pool_id
let dummy_pool_id = PoolId::new(Uint256::from_u64(0).into());
let dummy_stake_output = make_stake_output(
dummy_pool_id,
StakePoolCreationResolvedArguments {
amount: stake_pool_arguments.amount,
margin_ratio_per_thousand: stake_pool_arguments.margin_ratio_per_thousand,
cost_per_block: stake_pool_arguments.cost_per_block,
decommission_key: stake_pool_arguments.decommission_key,
staker_key: staker,
vrf_public_key,
},
);
let request = SendRequest::new().with_outputs([dummy_stake_output]);
let mut request = self.select_inputs_for_send_request(
request,
SelectedInputs::Utxos(vec![]),
None,
BTreeMap::new(),
self.create_stake_pool_tx_impl(
stake_pool_arguments,
db_tx,
vrf_public_key,
median_time,
fee_rate,
None,
)?;

let input0_outpoint = crate::utils::get_first_utxo_outpoint(request.inputs())?;
let new_pool_id = pos_accounting::make_pool_id(input0_outpoint);

// update the dummy_pool_id with the new pool_id
let old_pool_id = request
.get_outputs_mut()
.iter_mut()
.find_map(|out| match out {
TxOutput::CreateStakePool(pool_id, _) if *pool_id == dummy_pool_id => Some(pool_id),
TxOutput::CreateStakePool(_, _)
| TxOutput::Burn(_)
| TxOutput::Transfer(_, _)
| TxOutput::DelegateStaking(_, _)
| TxOutput::LockThenTransfer(_, _, _)
| TxOutput::CreateDelegationId(_, _)
| TxOutput::ProduceBlockFromStake(_, _)
| TxOutput::IssueFungibleToken(_)
| TxOutput::IssueNft(_, _, _)
| TxOutput::DataDeposit(_)
| TxOutput::Htlc(_, _)
| TxOutput::CreateOrder(_) => None,
})
.expect("find output with dummy_pool_id");
*old_pool_id = new_pool_id;

Ok(request)
)
}
}

Expand Down
20 changes: 16 additions & 4 deletions wallet/src/send_request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::collections::BTreeMap;
use std::mem::take;

use common::address::Address;
use common::chain::htlc::HtlcSecret;
use common::chain::output_value::OutputValue;
use common::chain::stakelock::StakePoolData;
use common::chain::timelock::OutputTimeLock::ForBlockCount;
Expand Down Expand Up @@ -51,6 +52,8 @@ pub struct SendRequest {

outputs: Vec<TxOutput>,

htlc_secrets: Vec<Option<HtlcSecret>>,

fees: BTreeMap<Currency, Amount>,
}

Expand Down Expand Up @@ -196,6 +199,7 @@ impl SendRequest {
destinations: Vec::new(),
inputs: Vec::new(),
outputs: Vec::new(),
htlc_secrets: Vec::new(),
fees: BTreeMap::new(),
}
}
Expand Down Expand Up @@ -230,6 +234,7 @@ impl SendRequest {
destinations,
inputs: transaction.inputs().to_vec(),
outputs: transaction.outputs().to_vec(),
htlc_secrets: vec![None; transaction.inputs().len()],
fees: BTreeMap::new(),
})
}
Expand Down Expand Up @@ -258,28 +263,35 @@ impl SendRequest {
self.inputs.push(outpoint);
self.destinations.push(destination);
self.utxos.push(None);
self.htlc_secrets.push(None);
}

self
}

pub fn with_inputs<'a, PoolDataGetter>(
mut self,
utxos: impl IntoIterator<Item = (TxInput, TxOutput)>,
utxos: impl IntoIterator<Item = (TxInput, TxOutput, Option<HtlcSecret>)>,
pool_data_getter: &PoolDataGetter,
) -> WalletResult<Self>
where
PoolDataGetter: Fn(&PoolId) -> Option<&'a PoolData>,
{
for (outpoint, txo) in utxos {
for (outpoint, txo, secret) in utxos {
self.inputs.push(outpoint);
let htlc_spending_condition = match &secret {
Some(_) => HtlcSpendingCondition::WithSecret,
None => HtlcSpendingCondition::WithMultisig,
};

self.destinations.push(
get_tx_output_destination(&txo, &pool_data_getter, HtlcSpendingCondition::Skip)
get_tx_output_destination(&txo, &pool_data_getter, htlc_spending_condition)
.ok_or_else(|| {
WalletError::UnsupportedTransactionOutput(Box::new(txo.clone()))
})?,
);
self.utxos.push(Some(txo));
self.htlc_secrets.push(secret);
}

Ok(self)
Expand Down Expand Up @@ -312,7 +324,7 @@ impl SendRequest {
vec![None; num_inputs],
utxos,
destinations,
None,
Some(self.htlc_secrets),
additional_info,
)?;
Ok(ptx)
Expand Down
Loading

0 comments on commit 36c9a71

Please sign in to comment.