partner_chains_cardano_offchain/
multisig.rs

1use crate::{
2	await_tx::AwaitTx,
3	cardano_keys::CardanoPaymentSigningKey,
4	csl::{
5		Costs, OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, get_builder_config,
6		key_hash_address,
7	},
8	governance::GovernanceData,
9};
10use cardano_serialization_lib::{
11	Address, JsError, NetworkIdKind, PrivateKey, Transaction, TransactionBody, TransactionBuilder,
12	TransactionOutput, Value,
13};
14use ogmios_client::{
15	query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
16	query_network::QueryNetwork,
17	transactions::Transactions,
18};
19use serde::{Serialize, Serializer};
20use sidechain_domain::{McTxHash, UtxoId, UtxoIndex, crypto::blake2b};
21
22/// Successful smart contracts offchain results in either transaction submission or creating transaction that has to be signed by the governance authorities
23#[derive(Clone, Debug, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum MultiSigSmartContractResult {
26	/// Result signifying that transaction has been submitted successfully.
27	TransactionSubmitted(McTxHash),
28	/// Result signifying that transaction requires more signatures by governance authorities.
29	TransactionToSign(MultiSigTransactionData),
30}
31
32impl MultiSigSmartContractResult {
33	/// Constructs [MultiSigSmartContractResult] from raw transaction hash bytes.
34	pub fn tx_submitted(hash: [u8; 32]) -> Self {
35		Self::TransactionSubmitted(McTxHash(hash))
36	}
37}
38
39/// MultiSig transactions awaiting for signatures use temporary wallets where funds are stored until the transaction is signed and submitted.
40/// This prevents payment utxo from being spend when the signatures for MultiSig are being collected.
41#[derive(Clone, Debug, Serialize)]
42pub struct MultiSigTransactionData {
43	/// Name of the transaction.
44	pub tx_name: String,
45	/// Temporary wallet data.
46	pub temporary_wallet: TemporaryWalletData,
47	#[serde(serialize_with = "serialize_as_conway_tx")]
48	/// Transaction CBOR bytes.
49	pub tx: Vec<u8>,
50}
51
52/// To be used only for manual re-claim of the funds if transaction has not been submitted.
53#[derive(Clone, Debug, Serialize)]
54pub struct TemporaryWalletData {
55	/// Wallet address.
56	pub address: String,
57	/// Wallet public key hash.
58	pub public_key_hash: String,
59}
60
61pub(crate) struct TemporaryWallet {
62	pub address: cardano_serialization_lib::Address,
63	pub private_key: CardanoPaymentSigningKey,
64}
65
66impl TemporaryWallet {
67	pub(crate) fn address_bech32(&self) -> String {
68		self.address.to_bech32(None).expect("to_bech32 is safe with None prefix")
69	}
70}
71
72impl From<TemporaryWallet> for TemporaryWalletData {
73	fn from(value: TemporaryWallet) -> Self {
74		TemporaryWalletData {
75			address: value.address_bech32(),
76			public_key_hash: hex::encode(&value.private_key.to_pub_key_hash().0),
77		}
78	}
79}
80
81fn serialize_as_conway_tx<S>(tx_bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
82where
83	S: Serializer,
84{
85	let json = serde_json::json!({
86		"type": "Tx ConwayEra",
87		"description": "",
88		"cborHex": hex::encode(tx_bytes)
89	});
90	json.serialize(serializer)
91}
92
93pub(crate) async fn fund_temporary_wallet<
94	F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
95	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
96	A: AwaitTx,
97>(
98	make_tx: &F,
99	payment_ctx: TransactionContext,
100	client: &T,
101	await_tx: &A,
102) -> anyhow::Result<TemporaryWallet> {
103	let wallet = create_temporary_wallet(payment_ctx.network)?;
104	let tx_to_estimate_costs = Costs::calculate_costs(|c| make_tx(c, &payment_ctx), client).await?;
105	let value = estimate_required_value(tx_to_estimate_costs.body(), &payment_ctx)?;
106	save_wallet_file(&wallet)?;
107	transfer_to_temporary_wallet(payment_ctx, &wallet.address, &value, client, await_tx).await?;
108	Ok(wallet)
109}
110
111fn create_temporary_wallet(network: NetworkIdKind) -> Result<TemporaryWallet, JsError> {
112	let private_key = CardanoPaymentSigningKey(PrivateKey::generate_ed25519()?);
113	let address = key_hash_address(&private_key.0.to_public().hash(), network);
114	Ok(TemporaryWallet { private_key, address })
115}
116
117/// Estimates required value by subtracting change, fee and collateral from the sum of inputs.
118/// Additional 5 ADA is subtracted, because multi assets present in inputs and change outputs,
119/// affect also coin present in the change and can make the calculated required value too low.
120fn estimate_required_value(
121	tx: TransactionBody,
122	ctx: &TransactionContext,
123) -> Result<Value, JsError> {
124	let mut change = Value::new(&0u32.into());
125	for output in tx.outputs().into_iter() {
126		if output.address() == ctx.change_address {
127			change = change.checked_add(&output.amount())?;
128		}
129	}
130
131	let mut total_input = Value::new(&0u32.into());
132	for input in tx.inputs().into_iter() {
133		if let Some(utxo) =
134			ctx.payment_key_utxos.iter().find(|utxo| utxo.to_csl_tx_input() == *input)
135		{
136			total_input = total_input.checked_add(&utxo.to_csl()?.output().amount())?;
137		}
138	}
139
140	total_input.clamped_sub(&change).checked_add(&Value::new(&5_000_000u32.into()))
141}
142
143fn save_wallet_file(wallet: &TemporaryWallet) -> Result<(), anyhow::Error> {
144	let key_bytes = wallet.private_key.to_bytes();
145	// CBOR wrappring the private key bytes. We don't have types to express this conveniently.
146	let cbor_hex = format!("5820{}", hex::encode(key_bytes));
147	let json = serde_json::json!({
148		"type": "PaymentSigningKeyShelley_ed25519",
149		"description": "Temporary wallet key generated for a MultiSigTransaction",
150		"cborHex": cbor_hex
151	});
152	let file_name = format!("{}.skey", wallet.address_bech32());
153	let file = std::fs::File::create(file_name)?;
154	serde_json::to_writer_pretty(file, &json)?;
155	Ok(())
156}
157
158async fn transfer_to_temporary_wallet<T: Transactions + QueryUtxoByUtxoId, A: AwaitTx>(
159	payment_ctx: TransactionContext,
160	address: &Address,
161	value: &Value,
162	client: &T,
163	await_tx: &A,
164) -> Result<(), anyhow::Error> {
165	let mut funding_tx_builder = TransactionBuilder::new(&get_builder_config(&payment_ctx)?);
166	funding_tx_builder.add_output(&TransactionOutput::new(address, value))?;
167	let funding_tx = funding_tx_builder.balance_update_and_build(&payment_ctx)?;
168	let tx_hash: [u8; 32] = blake2b(funding_tx.body().to_bytes().as_ref());
169	log::info!(
170		"Founding temporary wallet {} with {} in transaction: {}",
171		&address.to_bech32(None)?,
172		serde_json::to_string(&value)?,
173		&hex::encode(tx_hash)
174	);
175	client.submit_transaction(&payment_ctx.sign(&funding_tx).to_bytes()).await?;
176	await_tx.await_tx_output(client, UtxoId::new(tx_hash, 0)).await?;
177	Ok(())
178}
179
180/// If the chain has real MultiSig governance it:
181/// * creates a temporary wallet
182/// * sends 5 ADA from the payment wallet (subject of change)
183/// * creates a transaction that would be paid from the temporary wallet, signed by both wallets.
184///
185/// If the chain has single key MultiSig governance it creates and submits transaction paid by and signed by the payment wallet.
186pub(crate) async fn submit_or_create_tx_to_sign<F, T, A>(
187	governance_data: &GovernanceData,
188	payment_ctx: TransactionContext,
189	make_tx: F,
190	tx_name: &str,
191	client: &T,
192	await_tx: &A,
193) -> anyhow::Result<MultiSigSmartContractResult>
194where
195	F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
196	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
197	A: AwaitTx,
198{
199	Ok(if governance_data.policy.is_single_key_policy_for(&payment_ctx.payment_key_hash()) {
200		MultiSigSmartContractResult::TransactionSubmitted(
201			submit_single_governance_key_tx(payment_ctx, make_tx, tx_name, client, await_tx)
202				.await?,
203		)
204	} else {
205		MultiSigSmartContractResult::TransactionToSign(
206			create_transaction_to_sign(payment_ctx, make_tx, tx_name, client, await_tx).await?,
207		)
208	})
209}
210
211async fn submit_single_governance_key_tx<F, T, A>(
212	payment_ctx: TransactionContext,
213	make_tx: F,
214	tx_name: &str,
215	client: &T,
216	await_tx: &A,
217) -> anyhow::Result<McTxHash>
218where
219	F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
220	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
221	A: AwaitTx,
222{
223	let tx = Costs::calculate_costs(|c| make_tx(c, &payment_ctx), client).await?;
224	let signed_tx = payment_ctx.sign(&tx).to_bytes();
225	let res = client.submit_transaction(&signed_tx).await.map_err(|e| {
226		anyhow::anyhow!(
227			"Submit '{}' transaction request failed: {}, bytes: {}",
228			tx_name,
229			e,
230			hex::encode(signed_tx)
231		)
232	})?;
233	let tx_id = McTxHash(res.transaction.id);
234	log::info!("'{}' transaction submitted: {}", tx_name, hex::encode(tx_id.0));
235	await_tx
236		.await_tx_output(
237			client,
238			UtxoId { tx_hash: McTxHash(res.transaction.id), index: UtxoIndex(0) },
239		)
240		.await?;
241	Ok(tx_id)
242}
243
244async fn create_transaction_to_sign<F, T, A>(
245	payment_ctx: TransactionContext,
246	make_tx: F,
247	tx_name: &str,
248	client: &T,
249	await_tx: &A,
250) -> anyhow::Result<MultiSigTransactionData>
251where
252	F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
253	T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
254	A: AwaitTx,
255{
256	let original_ctx = payment_ctx.clone();
257	let temporary_wallet = fund_temporary_wallet(&make_tx, payment_ctx, client, await_tx)
258		.await
259		.map_err(|e| {
260			anyhow::anyhow!("Failed to create temporary wallet for '{}': {}", tx_name, e)
261		})?;
262	let temp_wallet_ctx =
263		TransactionContext::for_payment_key(&temporary_wallet.private_key, client)
264			.await?
265			.with_change_address(&original_ctx.change_address);
266	let tx =
267		Costs::calculate_costs(|c| make_tx(c, &temp_wallet_ctx), client)
268			.await
269			.map_err(|e| {
270				anyhow::anyhow!(
271					"Failed to create '{}' transaction using temporary wallet: {}",
272					tx_name,
273					e
274				)
275			})?;
276	let signed_tx_by_caller = original_ctx.sign(&tx);
277	let signed_tx = temp_wallet_ctx.sign(&signed_tx_by_caller);
278	Ok(MultiSigTransactionData {
279		tx_name: tx_name.to_owned(),
280		temporary_wallet: temporary_wallet.into(),
281		tx: signed_tx.to_bytes(),
282	})
283}