Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Partner Chain separately for use in `pallet_session_validator_management` and
other pallets that use cross-chain keys.
* `sp_sidechain::GetEpochDurationApi` runtime API
* Reusable migration `AuthorityKeysMigration` in `pallet_session_validator_management`
* Added `--out-file` parameter to governed-map commands (`insert`, `update`, `remove`) to save transaction CBOR hex directly to a file, simplifying multisig workflows
* Enhanced `sign-tx` command to accept multiple input formats: file path to JSON, JSON string, or direct CBOR hex, improving flexibility in multisig transaction workflows

# v1.8.0

Expand Down
19 changes: 14 additions & 5 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -983,12 +983,16 @@ $ pc-node sign-tx
--payment-key-file <PAYMENT_KEY_FILE>
```

`TRANSACTION` should be a CBOR-encoded transaction. The command will output a CBOR-encoded
witness that can be passed to `assemble-and-submit-tx`.
`TRANSACTION` can be:
- A file path to a JSON file containing the transaction
- A JSON string with the transaction
- A CBOR-encoded transaction hex string

The command will output a CBOR-encoded witness that can be passed to `assemble-and-submit-tx`.

##### governed-map

Set of subcommands for managing the Governed Map key-value store on Cardano
Set of subcommands for managing the Governed Map key-value store on Cardano. When multisig governance is configured, these commands support an optional `--out-file` parameter to save the transaction CBOR hex directly to a file for easier sharing and signing.

###### insert

Expand All @@ -1000,9 +1004,10 @@ $ pc-node governed-map insert
--value <VALUE>
--payment-key-file <PAYMENT_KEY_FILE>
--genesis-utxo <GENESIS_UTXO>
[ --out-file <FILE_PATH> ]
```

If the value for the key already exists it won't be updated.
If the value for the key already exists it won't be updated. The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use.

###### update

Expand All @@ -1015,11 +1020,12 @@ $ pc-node governed-map update
--payment-key-file <PAYMENT_KEY_FILE>
--genesis-utxo <GENESIS_UTXO>
[ --current-value <CURRENT_VALUE> ]
[ --out-file <FILE_PATH> ]
```

If the key does not already exist it won't be inserted.
If the optional `--current-value` argument is passed, the transaction will fail if the current
value doesn't match the argument.
value doesn't match the argument. The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use.

###### remove

Expand All @@ -1030,8 +1036,11 @@ $ pc-node governed-map remove
--key <KEY>
--payment-key-file <PAYMENT_KEY_FILE>
--genesis-utxo <GENESIS_UTXO>
[ --out-file <FILE_PATH> ]
```

The optional `--out-file` parameter saves the transaction CBOR hex to the specified file when multisig governance is in use.

###### list

Lists all key-value pairs currently stored in the Governed Map.
Expand Down
14 changes: 9 additions & 5 deletions docs/user-guides/governance/governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ In version v1.4 this functionality is available in the smart contracts CLI appli

## Multi Signature Governance

All the `smart-contracts` sub-commands that require Governance: `governance update`, `upsert-d-parameter`, `upsert-permissioned-candidates`, `reserve init|create|deposit|handover|update-settings`, and `governed-map insert|update|remove` will now submit the transaction only if the governance is "1 of 1". Otherwise these commands return a transaction CBOR that can be submitted with the new command `assemble-and-submit-tx`. Signatures can be obtained using `sign-tx`. Example of executed commands, invoked by owners of `key1` and `key2` are:
All the `smart-contracts` sub-commands that require Governance: `governance update`, `upsert-d-parameter`, `upsert-permissioned-candidates`, `reserve init|create|deposit|handover|update-settings`, and `governed-map insert|update|remove` will now submit the transaction only if the governance is "1 of 1". Otherwise these commands return a transaction CBOR that can be submitted with the new command `assemble-and-submit-tx`. Signatures can be obtained using `sign-tx`.

For convenience, governance commands support an optional `--out-file` parameter that saves the transaction CBOR hex directly to a file, eliminating the need for manual extraction using tools like `jq`. Example of executed commands, invoked by owners of `key1` and `key2` are:

Owner of `key1` initialized Governance with two key hashes `e8c300330fe315531ca89d4a2e7d0c80211bc70b473b1ed4979dff2b` and `7fa48bb8fb5d6804fad26237738ce490d849e4567161e38ab8415ff3`(that are hashes of `key1` and `key2`), and sets requried number of signatures to `2`.
```
Expand All @@ -100,15 +102,17 @@ Owner of `key1` wants to set D-parameter to (7, 5).
-c f8fbe7316561e57de9ecd1c86ee8f8b512a314ba86499ba9a584bfa8fe2edc8d#0 \
--permissioned-candidates-count 7 \
--registered-candidates-count 5 \
-k key1.skey | jq .
-k key1.skey \
--out-file tx.cbor
...
✓ Wrote transaction CBOR hex to: tx.cbor
{"transaction_to_sign":{"temporary_wallet":{"address":"addr_test1vzeg2g6gcnlvnemk9hgvsaxxktf8suxwd63hm54w9erxuwc49exyq","funded_by_tx":"0xed99c5eb6d12053c514915fcb0445c9ce9839b65570db042fcd1c9d9cc9fbcf8","private_key":"0x730f9c6f26666da41dedbe596f6b2f7d36a98ce768591010b537e4f48417448f"},"tx":{"cborHex":"84aa00..transaction bytes redacted..f6","description":"","type":"Tx ConwayEra"},"tx_name":"Insert D-parameter"}}
```
The user gets the transaction data. It already contains the `key1` signature. Transaction requires signature of `key2` owner, before it can be submitted.
The user gets the transaction data. It already contains the `key1` signature. The transaction CBOR hex has been saved to `tx.cbor`. Transaction requires signature of `key2` owner, before it can be submitted.

`key2` owner has to get this transaction data, perhaps from `key1` owner, *inspect the transaction* and then sign `cborHex` value from the previous output:
`key2` owner has to get this transaction data, perhaps from `key1` owner, *inspect the transaction* and then sign it. The `sign-tx` command can accept the CBOR hex as a file path, JSON string, or direct hex:
```
./partner-chains-node smart-contracts sign-tx -k key2.skey --transaction 84aa00..<transaction-bytes-redacted>..f6
./partner-chains-node smart-contracts sign-tx -k key2.skey --transaction tx.cbor
...
{"cborHex":"82008258202bebcb7fbc74a6e0fd6e00a311698b047b7b659f0e047ff5349dbd984aefc52c58409dfff5d837ec7b864502c7acac5ad5885f74d94cb68458413ee4565ff52f6dcb1ff3df272566662b4f00766fc9586a12532bfce68e56280f93dd57d6e22b9705","description":"","type":"TxWitness ConwayEra"}
```
Expand Down
40 changes: 36 additions & 4 deletions toolkit/smart-contracts/commands/src/governed_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use partner_chains_cardano_offchain::governed_map::{
use serde_json::json;
use sidechain_domain::byte_string::ByteString;
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Clone, Debug, clap::Subcommand)]
#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -55,6 +56,9 @@ pub struct InsertCmd {
#[clap(flatten)]
/// Genesis UTXO
genesis_utxo: GenesisUtxo,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
/// Optional file path to save the transaction CBOR hex (for multisig flow)
out_file: Option<PathBuf>,
}

impl InsertCmd {
Expand All @@ -73,7 +77,7 @@ impl InsertCmd {
&self.common_arguments.retries(),
)
.await;
print_result_json(result)
print_result_json(result, self.out_file)
}
}

Expand All @@ -97,6 +101,9 @@ pub struct UpdateCmd {
#[clap(flatten)]
/// Genesis UTXO
genesis_utxo: GenesisUtxo,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
/// Optional file path to save the transaction CBOR hex (for multisig flow)
out_file: Option<PathBuf>,
}

impl UpdateCmd {
Expand All @@ -116,7 +123,7 @@ impl UpdateCmd {
&self.common_arguments.retries(),
)
.await;
print_result_json(result)
print_result_json(result, self.out_file)
}
}

Expand All @@ -134,6 +141,9 @@ pub struct RemoveCmd {
#[clap(flatten)]
/// Genesis UTXO
genesis_utxo: GenesisUtxo,
#[arg(long, value_hint = clap::ValueHint::FilePath)]
/// Optional file path to save the transaction CBOR hex (for multisig flow)
out_file: Option<PathBuf>,
}

impl RemoveCmd {
Expand All @@ -151,7 +161,7 @@ impl RemoveCmd {
&self.common_arguments.retries(),
)
.await;
print_result_json(result)
print_result_json(result, self.out_file)
}
}

Expand Down Expand Up @@ -205,12 +215,34 @@ impl GetCmd {
}

/// Converts the result of a command into a JSON object.
/// If out_file is provided and the result is a TransactionToSign, saves the cborHex to the file.
fn print_result_json(
result: anyhow::Result<Option<crate::MultiSigSmartContractResult>>,
out_file: Option<PathBuf>,
) -> crate::SubCmdResult {
match result {
Err(err) => Err(err)?,
Ok(Some(res)) => Ok(json!(res)),
Ok(Some(res)) => {
// If out_file is specified and this is a multisig transaction, save cborHex
if let (Some(path), crate::MultiSigSmartContractResult::TransactionToSign(tx_data)) =
(&out_file, &res)
{
// Extract cborHex from the serialized transaction
let tx_json = serde_json::to_value(&tx_data.tx)?;
if let Some(cbor_hex) = tx_json.get("cborHex").and_then(|v| v.as_str()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have tx_data.tx that are bytes. Then you serialized it Cardano file JSON and then you take this field containing bytes of the transaction instead of using it directly.

You could just hex::encode(tx_data.tx), couldn't you?

std::fs::write(path, cbor_hex).map_err(|e| {
anyhow::anyhow!("Failed to write CBOR to file {}: {}", path.display(), e)
})?;
eprintln!("✓ Wrote transaction CBOR hex to: {}", path.display());
} else {
return Err(anyhow::anyhow!(
"--out-file specified but could not extract cborHex from transaction"
)
.into());
}
}
Ok(json!(res))
},
Ok(None) => Ok(json!({})),
}
}
63 changes: 59 additions & 4 deletions toolkit/smart-contracts/commands/src/sign_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ use sidechain_domain::TransactionCbor;
#[derive(Clone, Debug, clap::Parser)]
/// Command for signing a cardano transaction
pub struct SignTxCmd {
#[arg(long)]
/// Hex-encoded transaction CBOR (with or without 0x prefix)
transaction: TransactionCbor,
#[arg(
long,
value_hint = clap::ValueHint::AnyPath,
help = "Transaction input: hex string, JSON with transaction_to_sign, or file path to JSON"
)]
transaction: String,
#[clap(flatten)]
/// Path to the Cardano Signing Key file that you want to sign the transaction with
payment_key_file: PaymentFilePath,
Expand All @@ -19,7 +22,13 @@ impl SignTxCmd {
pub async fn execute(self) -> crate::SubCmdResult {
let payment_key = self.payment_key_file.read_key()?;

let vkey_witness = sign_tx(self.transaction.0, &payment_key)?;
// Try to extract cborHex from the input
let cbor_hex = extract_cbor_hex(&self.transaction)?;
let transaction_cbor: TransactionCbor = cbor_hex
.parse()
.map_err(|e| anyhow::anyhow!("Failed to parse transaction CBOR: {}", e))?;

let vkey_witness = sign_tx(transaction_cbor.0, &payment_key)?;

let json = json!(
{
Expand All @@ -31,3 +40,49 @@ impl SignTxCmd {
Ok(json)
}
}

/// Extracts cborHex from the input string.
/// Uses deterministic order: file path -> JSON parsing -> hex string
fn extract_cbor_hex(input: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let trimmed = input.trim();

// Case 1: Check if it's a file path that exists
let json_str = if std::path::Path::new(trimmed).exists() {
std::fs::read_to_string(trimmed)
.map_err(|e| format!("Failed to read file '{}': {}", trimmed, e))?
} else if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(trimmed) {
// Case 2: Try parsing as JSON directly
return extract_cbor_from_json(&json_value);
} else {
// Case 3: Treat as direct hex string
return Ok(trimmed.to_string());
};

// If we read from file, parse the JSON and extract cborHex
let json_value: serde_json::Value = serde_json::from_str(&json_str)
.map_err(|e| format!("Failed to parse JSON from file: {}", e))?;

extract_cbor_from_json(&json_value)
}

/// Extracts cborHex from a JSON value
fn extract_cbor_from_json(
json_value: &serde_json::Value,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Try to extract cborHex from transaction_to_sign format
if let Some(cbor_hex) = json_value
.get("transaction_to_sign")
.and_then(|v| v.get("tx"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A this point I would parse MultiSigTransactionData instead of manually traversing its format

.and_then(|v| v.get("cborHex"))
.and_then(|v| v.as_str())
{
return Ok(cbor_hex.to_string());
}

// Try to extract cborHex from direct tx format
if let Some(cbor_hex) = json_value.get("cborHex").and_then(|v| v.as_str()) {
return Ok(cbor_hex.to_string());
}

Err("Could not extract cborHex from JSON. Expected 'transaction_to_sign.tx.cborHex' or 'cborHex' field.".into())
}
Loading