respect slate version

This commit is contained in:
wiesche
2026-09-07 18:53:36 +02:00
parent 538040865a
commit c613d82daf
9 changed files with 110 additions and 44 deletions
+54 -18
View File
@@ -2119,6 +2119,11 @@ pub trait OwnerRpc {
) -> Result<SwapReq, Error>;
}
fn versioned_output(slate: Slate) -> Result<VersionedSlate, Error> {
let version = SlateVersion::output_for(&slate)?;
VersionedSlate::into_version(slate, version)
}
impl<L, C, K> OwnerRpc for Owner<L, C, K>
where
L: WalletLCProvider<'static, C, K>,
@@ -2216,8 +2221,7 @@ where
fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result<VersionedSlate, Error> {
let slate = Owner::init_send_tx(self, (&token.keychain_mask).as_ref(), args)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(slate, version)
versioned_output(slate)
}
fn contract_new(
@@ -2238,20 +2242,13 @@ where
in_slate: VersionedSlate,
args: ContractSetupArgsAPI,
) -> Result<VersionedSlate, Error> {
let in_version = in_slate.version();
let slate = Owner::contract_sign(
self,
(&token.keychain_mask).as_ref(),
&Slate::from(in_slate),
&args,
)?;
// Keep the version the counterparty sent us, since they can clearly read it, but
// never below what the slate needs to represent its payment proof.
let version = match SlateVersion::lowest_for(&slate) {
SlateVersion::V5 => SlateVersion::V5,
SlateVersion::V4 => in_version,
};
VersionedSlate::into_version(slate, version)
versioned_output(slate)
}
fn contract_view(&self, token: Token, slate: VersionedSlate) -> Result<ContractView, Error> {
@@ -2278,8 +2275,7 @@ where
args: IssueInvoiceTxArgs,
) -> Result<VersionedSlate, Error> {
let slate = Owner::issue_invoice_tx(self, (&token.keychain_mask).as_ref(), args)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(slate, version)
versioned_output(slate)
}
fn process_invoice_tx(
@@ -2294,8 +2290,7 @@ where
&Slate::from(in_slate),
args,
)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(out_slate, version)
versioned_output(out_slate)
}
fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), Error> {
@@ -2312,8 +2307,7 @@ where
(&token.keychain_mask).as_ref(),
&Slate::from(in_slate),
)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(out_slate, version)
versioned_output(out_slate)
}
fn post_tx(&self, token: Token, slate: VersionedSlate, fluff: bool) -> Result<(), Error> {
@@ -2538,8 +2532,7 @@ where
secret_indices,
)?;
// Keep the decoded version; lowest_for would downgrade a plain V5 slate
let version = SlateVersion::try_from(slate.version_info.version)?;
VersionedSlate::into_version(slate, version)
versioned_output(slate)
}
fn decode_slatepack_message(
@@ -2900,6 +2893,18 @@ mod tests {
use grin_wallet_util::byte_ser;
use tempfile::tempdir;
#[test]
fn output_keeps_version() {
for version in [4, 5] {
let mut slate = Slate::blank(2, false);
slate.version_info.version = version;
assert_eq!(
versioned_output(slate).unwrap().version(),
SlateVersion::try_from(version).unwrap()
);
}
}
#[test]
fn slatepack_keeps_v5() {
let slate = VersionedSlate::into_version(Slate::blank(2, false), SlateVersion::V5).unwrap();
@@ -2932,4 +2937,35 @@ mod tests {
assert_eq!(response["result"]["Ok"]["ver"], "5:3");
}
#[test]
fn invoice_uses_target_version() {
let request = serde_json::json!({
"jsonrpc": "2.0",
"method": "issue_invoice_tx",
"params": {
"token": "d202964900000000d302964900000000d402964900000000d502964900000000",
"args": {
"amount": "6000000000",
"dest_acct_name": null,
"target_slate_version": 5
}
},
"id": 1
});
let dir = tempdir().unwrap();
let response = run_doctest_owner(
request,
dir.path().to_str().unwrap(),
0,
false,
false,
false,
false,
)
.unwrap()
.unwrap();
assert_eq!(response["result"]["Ok"]["ver"], "5:1");
}
}
+18 -8
View File
@@ -84,7 +84,9 @@ fn slatepack_exchange_test_impl(
use_bin: bool,
use_armored: bool,
use_encryption: bool,
target_slate_version: Option<u16>,
) -> Result<(), libwallet::Error> {
let expected_slate_version = target_slate_version.unwrap_or(4);
// Create a new proxy to simulate server and wallet responses
let mut wallet_proxy = create_wallet_proxy(test_dir);
let chain = wallet_proxy.chain.clone();
@@ -236,6 +238,7 @@ fn slatepack_exchange_test_impl(
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: true,
target_slate_version,
..Default::default()
};
let slate = api.init_send_tx(m, args)?;
@@ -261,6 +264,7 @@ fn slatepack_exchange_test_impl(
let (mut slatepack, mut slate) =
slate_from_packed(&send_file, use_armored, (&dec_key_2).as_ref())?;
assert_eq!(slate.version_info.version, expected_slate_version);
// wallet 2 receives file, completes, sends file back
wallet::controller::foreign_single_use(
@@ -293,7 +297,9 @@ fn slatepack_exchange_test_impl(
|api, m| {
let (_, mut slate) =
slate_from_packed(&receive_file, use_armored, (&dec_key_1).as_ref())?;
assert_eq!(slate.version_info.version, expected_slate_version);
slate = api.finalize_tx(m, &slate)?;
assert_eq!(slate.version_info.version, expected_slate_version);
// Output final file for reference
output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?;
api.post_tx(m, &slate, false)?;
@@ -357,6 +363,7 @@ fn slatepack_exchange_test_impl(
|api, m| {
let args = IssueInvoiceTxArgs {
amount: 1000000000,
target_slate_version,
..Default::default()
};
slate = api.issue_invoice_tx(m, args)?;
@@ -389,6 +396,7 @@ fn slatepack_exchange_test_impl(
let res = slate_from_packed(&send_file, use_armored, (&dec_key_1).as_ref())?;
slatepack = res.0;
slate = res.1;
assert_eq!(slate.version_info.version, expected_slate_version);
slate = api.process_invoice_tx(m, &slate, args)?;
api.tx_lock_outputs(m, &slate)?;
output_slatepack(
@@ -413,7 +421,9 @@ fn slatepack_exchange_test_impl(
// Wallet 2 receives the invoice transaction
let res = slate_from_packed(&receive_file, use_armored, (&dec_key_2).as_ref())?;
slate = res.1;
assert_eq!(slate.version_info.version, expected_slate_version);
slate = api.finalize_tx(&slate, false)?;
assert_eq!(slate.version_info.version, expected_slate_version);
output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?;
Ok(())
},
@@ -707,11 +717,11 @@ fn slatepack_address_validation(test_dir: &'static str) -> Result<(), libwallet:
}
#[test]
fn slatepack_exchange_json() {
let test_dir = "test_output/slatepack_exchange_json";
fn slatepack_exchange_json_v5() {
let test_dir = "test_output/slatepack_exchange_json_v5";
setup(test_dir);
// Json output
if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, false) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, false, Some(5)) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
@@ -722,7 +732,7 @@ fn slatepack_exchange_bin() {
let test_dir = "test_output/slatepack_exchange_bin";
setup(test_dir);
// Bin output
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, false) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, false, None) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
@@ -733,7 +743,7 @@ fn slatepack_exchange_armored() {
let test_dir = "test_output/slatepack_exchange_armored";
setup(test_dir);
// Bin output
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true, None) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
@@ -744,7 +754,7 @@ fn slatepack_exchange_json_enc() {
let test_dir = "test_output/slatepack_exchange_json_enc";
setup(test_dir);
// Json output
if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, true) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, true, None) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
@@ -755,7 +765,7 @@ fn slatepack_exchange_bin_enc() {
let test_dir = "test_output/slatepack_exchange_bin_enc";
setup(test_dir);
// Bin output
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, true) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, true, None) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
@@ -766,7 +776,7 @@ fn slatepack_exchange_armored_enc() {
let test_dir = "test_output/slatepack_exchange_armored_enc";
setup(test_dir);
// Bin output
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true) {
if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true, None) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
+11 -3
View File
@@ -42,9 +42,9 @@ use crate::{
address, contract,
mwixnet::{create_onion, ComSignature, Hop, MixnetReqCreationParams, SwapReq},
wallet_lock, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult,
OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack,
SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletBackend,
WalletInitStatus, WalletInst, WalletLCProvider,
OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, SlateVersion,
Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet,
WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider,
};
use ed25519_dalek::SigningKey as DalekSecretKey;
use ed25519_dalek::Verifier;
@@ -675,6 +675,10 @@ where
C: NodeClient,
K: Keychain,
{
if let Some(v) = args.target_slate_version {
SlateVersion::try_from(v)?;
}
let payment_proof_address = if let Some(a) = &args.payment_proof_recipient_address {
if a.valid_network() {
Some(a)
@@ -801,6 +805,10 @@ where
C: NodeClient,
K: Keychain,
{
if let Some(v) = args.target_slate_version {
SlateVersion::try_from(v)?;
}
let parent_key_id = match args.dest_acct_name {
Some(d) => {
let pm = w.get_acct_path(d)?;
+2 -4
View File
@@ -72,8 +72,7 @@ pub struct InitTxArgs {
#[serde(default = "default_refresh_outputs_from_node")]
pub refresh_outputs_from_node: bool,
/// Optionally set the output target slate version (acceptable
/// down to the minimum slate version compatible with the current. If `None` the slate
/// is generated with the latest version.
/// down to the minimum slate version compatible with the current. If `None`, V4 is used.
pub target_slate_version: Option<u16>,
/// Number of blocks from current after which TX should be ignored
#[serde(with = "secp_ser::opt_string_or_u64")]
@@ -142,8 +141,7 @@ pub struct IssueInvoiceTxArgs {
#[serde(with = "secp_ser::string_or_u64")]
pub amount: u64,
/// Optionally set the output target slate version (acceptable
/// down to the minimum slate version compatible with the current. If `None` the slate
/// is generated with the latest version.
/// down to the minimum slate version compatible with the current. If `None`, V4 is used.
pub target_slate_version: Option<u16>,
}
-1
View File
@@ -46,7 +46,6 @@ Separating side effects until the 'save_step' part would make these functions mu
- Preserve kernel features and their arguments across V4 and V5; see #793.
- Choose the sender derivation path explicitly and keep encrypted outgoing files
identifiable locally.
- Decide whether `target_slate_version` should control Slatepack output or be removed.
- Add contract history, lookup and transport.
- Decide whether the foreign JSON-RPC API should expose `new` and `sign`.
- Add full API and RPC examples once the contract interface is stable.
+2
View File
@@ -70,6 +70,8 @@ where
// Initialize a new contract (if net_change is positive, I'm the receiver meaning this is invoice flow)
let num_participants = setup_args.num_participants;
let mut slate = Slate::blank(num_participants, net_change > 0);
// Contracts start with V4 and move to V5 when their proof data requires it
slate.version_info.version = 4;
// Use a caller-supplied id when given, so a retried creation reuses the same context.
let mut reused_context = false;
if let Some(id) = slate_id {
+2
View File
@@ -57,6 +57,8 @@ where
{
let current_height = wallet.w2n_client().get_chain_tip()?.0;
let mut slate = Slate::blank(num_participants, is_invoice);
// Start with V4 for compatibility unless the caller requests a newer version
slate.version_info.version = 4;
if let Some(b) = ttl_blocks {
slate.ttl_cutoff_height = current_height + b;
}
+20 -7
View File
@@ -65,15 +65,23 @@ impl TryFrom<u16> for SlateVersion {
}
impl SlateVersion {
/// The lowest version that can represent this slate without losing data, so a slate
/// is only sent as V5 when it has to be. V4 carries the payment proof's sender and
/// receiver addresses and its signature, but not the proof type, timestamp or memo.
/// Use V5 only when the slate has fields that V4 cannot store
/// V4 does not support the proof type, timestamp or memo
pub fn lowest_for(slate: &Slate) -> SlateVersion {
match &slate.payment_proof {
Some(p) if p.requires_v5() => SlateVersion::V5,
_ => SlateVersion::V4,
}
}
/// The requested output version, raised when the slate needs a newer version
pub fn output_for(slate: &Slate) -> Result<SlateVersion, Error> {
let requested = SlateVersion::try_from(slate.version_info.version)?;
Ok(match SlateVersion::lowest_for(slate) {
SlateVersion::V5 => SlateVersion::V5,
SlateVersion::V4 => requested,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
@@ -318,7 +326,7 @@ pub mod tests {
}
#[test]
fn create_slatepack_emits_lowest_version() -> Result<(), Error> {
fn create_slatepack_uses_output_version() -> Result<(), Error> {
set_local_chain_type(ChainTypes::Mainnet);
let packer = Slatepacker::new(SlatepackerArgs {
sender: None,
@@ -334,16 +342,20 @@ pub mod tests {
assert_eq!(recovered_v5.version_info.version, 5);
assert!(recovered_v5.payment_proof.unwrap().memo.is_some());
// Without a proof, V4 is sufficient and is emitted for interoperability with V4 peers.
// A plain slate keeps its requested version.
let mut slate_no_proof = populate_test_slate()?;
slate_no_proof.payment_proof = None;
let recovered_v5 = packer.get_slate(&packer.create_slatepack(&slate_no_proof)?)?;
assert_eq!(recovered_v5.version_info.version, 5);
slate_no_proof.version_info.version = 4;
let recovered_v4 = packer.get_slate(&packer.create_slatepack(&slate_no_proof)?)?;
assert_eq!(recovered_v4.version_info.version, 4);
// A legacy send proof uses no V5 field. It must stay V4 so V4-only wallets can read it,
// and the proof has to survive the round trip intact.
// A legacy send proof also fits in V4 and survives the round trip intact.
let mut slate_legacy_proof = populate_test_slate()?;
use_legacy_proof(&mut slate_legacy_proof);
slate_legacy_proof.version_info.version = 4;
let proof = slate_legacy_proof.payment_proof.clone().unwrap();
let recovered_legacy = packer.get_slate(&packer.create_slatepack(&slate_legacy_proof)?)?;
assert_eq!(recovered_legacy.version_info.version, 4);
@@ -358,6 +370,7 @@ pub mod tests {
proof.timestamp = None;
proof.memo = None;
proof.promise_signature = None;
partial_proof.version_info.version = 4;
let recovered = packer.get_slate(&packer.create_slatepack(&partial_proof)?)?;
assert_eq!(recovered.version_info.version, 5);
let proof = recovered.payment_proof.unwrap();
+1 -3
View File
@@ -94,9 +94,7 @@ impl<'a> Slatepacker<'a> {
/// Create slatepack from slate and args
pub fn create_slatepack(&self, slate: &Slate) -> Result<Slatepack, Error> {
// Emit the lowest version that can represent this slate, for maximum
// interoperability with V4-only wallets.
let version = SlateVersion::lowest_for(slate);
let version = SlateVersion::output_for(slate)?;
let out_slate = VersionedSlate::into_version(slate.clone(), version)?;
let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?;
let mut slatepack = Slatepack::default();