[CONTRACTS] Contracts branch update for 1.80 (#718)

* Simple contracts restructured v3 (#675)

* Add prototype contract implementation

Lacks:
- Payment proofs (add early payment proofs)
- Tests
- Better structure

* Separate contract utilities

* Simplify the main setup/sign code flow

* Remove commented initial_sec_key assignment

* Simplify sign::compute function

* Add logic for "removal" of secret keys from the context

* Further simplify a bit sign and setup

* No need for mutable context when adding outputs

* Refactor the commented code (lol)

* Refactor a bit

* tmp

---------

Co-authored-by: oryhp <gtrphyro@gmail.com>

* contract test fixes (#676)

* [Contracts] Experimental Slate v5 (#677)

* beginning to add and modify slate version 5

* completion of conversions from V5 to V4 Slate

* timestamp and memo fields

* upgrade/downgrade serialization of v5 slates

* add v5 binary slate versions + start of tests

* add bin slate ser/deser to tests

* ensure serialization of timestamp always excludes milliseconds

* start to update v5 tests, update v5 documentation

* add fn to generate populated internal slate for conversion testing

* add basic tests to convert all slate versions

* update

* commit cargo.lock

* [WIP] [Contracts] Early payment proofs (#681)

* add types and beginnings of signature utils

* add proof serialization

* serialisation of proof data + signature operation

* add serialization type for invoice proof + separate bin wrapper version

* add witness data + serializion to invoice payment proof, insert verfication functions in place in order to begin verification testing

* tests and infrastructure in place for validation

* verification of promise sig

* added verification of promise signature, infrastructure up to the point where a signature must be subtracted

* attempting to figure out differences between recipient nonce that's getting stored and calculated recipient nonce

* implementation of witness verification function, retrieve relevant values and re-validate derived recipient partial signature

* move stored portion of invoice proof into core types for storage, need to rename invoice proof

* define/refine the stored portion of payment proofs type 2?

* Folding all proof data into tx log entry storage

* back to importing master

* remove cargo files from diffs

* remove a lot of extra debug output

* return proof witness as part of proof retrieval, define json serialization of invoice proof + witness fields

* finish adding verification steps to foreign API

* remove redundant promise sig field

* move lcation of sign/verify calls

* Replace Azure Pipelines with Github Actions (#688)

* Update CI Badge on README.MD (#690)

* Trigger CI on push and pull request (#693)

* Update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3 (#691)

* update versioning to 5.2.0-beta.1 against grin 5.2.0-beta.3

* tweak for CI trigger

---------

Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>

---------

Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>

* fix Cargo.lock

* Contracts - Test fixes + warning cleanups (#694)

* clean up warnings in libwallet crate

* clean up warnings in controller crate

* update all contract tests with awareness of new proof structure

* added explicit payment proof tests for both rsr and srs (#695)

* Basic mwmixnet send (#696)

* integrating onion library

* updates and changes to support newly included mwmixnet types

* add (incorrect) owner api function

* turn off test for now

* switch working grin branch to master

* fix doctests for build

* update cargo lock in attempt to fix croaring build on CI server

* update cargo lock with upstream thiserror crate

* update test dependency for croaring

* [Contracts] Add self spend transaction state (#697)

* Add self spend transaction state

* subtle errors with output states and tx lookups - fixes

* [Contracts] Slatepack v5 Deserialization fix (#698)

* add V5 deserialization test + fixes

* clarify comment

* upwrap fix during v4 deserialization

* further unwrap removal

* update cargo lock, use latest grin packages

* addition of self-spend cancel + tests (#699)

* add tests + legacy self send cancel (#702)

* [Contracts] Cancel self-spend TX Pt. 2 (#703)

* add tests + legacy self send cancel

* add missing file

* Update grin to latest master, change versioning label to include contracts (#712)

* Add owner_api_listen_interface as hidden configuration field (#714)

* updates for rust 1.80

* updates for rust 1.80

---------

Co-authored-by: oryhp <gtrphyro@gmail.com>
Co-authored-by: Quentin Le Sceller <q.lesceller@gmail.com>
This commit is contained in:
Yeastplume 2024-08-27 12:30:22 +01:00 committed by GitHub
parent 765f11b168
commit cdd63fb565
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 10170 additions and 220 deletions

141
Cargo.lock generated
View file

@ -330,11 +330,10 @@ checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
[[package]]
name = "built"
version = "0.4.4"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8f1b029cb3929cb0c99780b0c10fe512f60be5438adf5f757e4afa1bc75a984"
checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4"
dependencies = [
"cargo-lock",
"git2",
]
@ -378,18 +377,6 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cargo-lock"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8504b63dd1249fd1745b7b4ef9b6f7b107ddeb3c95370043c7dbcc38653a2679"
dependencies = [
"semver 0.9.0",
"serde",
"toml 0.5.11",
"url",
]
[[package]]
name = "cc"
version = "1.0.98"
@ -682,6 +669,41 @@ dependencies = [
"zeroize",
]
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
"proc-macro2 1.0.83",
"quote 1.0.36",
"strsim 0.10.0",
"syn 1.0.109",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"quote 1.0.36",
"syn 1.0.109",
]
[[package]]
name = "dashmap"
version = "5.5.3"
@ -913,12 +935,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "fake-simd"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
[[package]]
name = "fastrand"
version = "2.1.0"
@ -1190,11 +1206,11 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "git2"
version = "0.13.25"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.5.0",
"libc",
"libgit2-sys",
"log",
@ -1400,7 +1416,7 @@ dependencies = [
[[package]]
name = "grin_wallet"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"built",
"clap",
@ -1422,7 +1438,7 @@ dependencies = [
"remove_dir_all",
"rpassword",
"rustyline",
"semver 0.10.0",
"semver",
"serde",
"serde_derive",
"serde_json",
@ -1432,7 +1448,7 @@ dependencies = [
[[package]]
name = "grin_wallet_api"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"base64 0.12.3",
"chrono",
@ -1457,7 +1473,7 @@ dependencies = [
[[package]]
name = "grin_wallet_config"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"dirs 2.0.2",
"grin_core",
@ -1472,7 +1488,7 @@ dependencies = [
[[package]]
name = "grin_wallet_controller"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"chrono",
"easy-jsonrpc-mw",
@ -1508,7 +1524,7 @@ dependencies = [
[[package]]
name = "grin_wallet_impls"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"base64 0.12.3",
"blake2-rfc",
@ -1547,7 +1563,7 @@ dependencies = [
[[package]]
name = "grin_wallet_libwallet"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"age",
"base64 0.9.3",
@ -1555,15 +1571,18 @@ dependencies = [
"blake2-rfc",
"bs58",
"byteorder",
"chacha20",
"chrono",
"curve25519-dalek 2.1.3",
"ed25519-dalek",
"grin_core",
"grin_keychain",
"grin_secp256k1zkp",
"grin_store",
"grin_util",
"grin_wallet_config",
"grin_wallet_util",
"hmac 0.12.1",
"lazy_static",
"log",
"num-bigint",
@ -1573,7 +1592,8 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"sha2 0.8.2",
"serde_with",
"sha2 0.10.8",
"strum",
"strum_macros",
"thiserror",
@ -1583,7 +1603,7 @@ dependencies = [
[[package]]
name = "grin_wallet_util"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
dependencies = [
"data-encoding",
"ed25519-dalek",
@ -1898,6 +1918,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
@ -2049,9 +2075,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libgit2-sys"
version = "0.12.26+1.3.0"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
@ -3518,16 +3544,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
"serde",
]
[[package]]
name = "semver"
version = "0.10.0"
@ -3605,6 +3621,29 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
dependencies = [
"chrono",
"serde",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
dependencies = [
"darling",
"proc-macro2 1.0.83",
"quote 1.0.36",
"syn 1.0.109",
]
[[package]]
name = "serde_yaml"
version = "0.8.26"
@ -3617,18 +3656,6 @@ dependencies = [
"yaml-rust 0.4.5",
]
[[package]]
name = "sha2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
dependencies = [
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug 0.2.3",
]
[[package]]
name = "sha2"
version = "0.9.9"

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
license = "Apache-2.0"
@ -30,21 +30,21 @@ semver = "0.10"
rustyline = "6"
lazy_static = "1"
grin_wallet_api = { path = "./api", version = "5.4.0-alpha.1" }
grin_wallet_impls = { path = "./impls", version = "5.4.0-alpha.1" }
grin_wallet_libwallet = { path = "./libwallet", version = "5.4.0-alpha.1" }
grin_wallet_controller = { path = "./controller", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "./config", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "./util", version = "5.4.0-alpha.1" }
grin_wallet_api = { path = "./api", version = "5.4.0-contracts.0" }
grin_wallet_impls = { path = "./impls", version = "5.4.0-contracts.0" }
grin_wallet_libwallet = { path = "./libwallet", version = "5.4.0-contracts.0" }
grin_wallet_controller = { path = "./controller", version = "5.4.0-contracts.0" }
grin_wallet_config = { path = "./config", version = "5.4.0-contracts.0" }
grin_wallet_util = { path = "./util", version = "5.4.0-contracts.0" }
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_keychain = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_api = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_keychain = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# grin_api = "5.4.0-contracts.0"
# For beta release
@ -68,7 +68,7 @@ grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" }
######
[build-dependencies]
built = { version = "0.4", features = ["git2"]}
built = { version = "0.7", features = ["git2"]}
[dev-dependencies]
url = "2.1"

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_api"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Grin Wallet API"
license = "Apache-2.0"
@ -22,17 +22,17 @@ ring = "0.16"
base64 = "0.12"
ed25519-dalek = "1.0.0-pre.4"
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
grin_wallet_impls = { path = "../impls", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-contracts.0" }
grin_wallet_config = { path = "../config", version = "5.4.0-contracts.0" }
grin_wallet_impls = { path = "../impls", version = "5.4.0-contracts.0" }
grin_wallet_util = { path = "../util", version = "5.4.0-contracts.0" }
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_keychain = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_keychain = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# For beta release

View file

@ -17,6 +17,8 @@
use crate::config::TorConfig;
use crate::keychain::Keychain;
use crate::libwallet::api_impl::foreign;
use crate::libwallet::contract::proofs::InvoiceProof;
use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use crate::libwallet::{
BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst,
WalletLCProvider,
@ -24,6 +26,7 @@ use crate::libwallet::{
use crate::try_slatepack_sync_workflow;
use crate::util::secp::key::SecretKey;
use crate::util::Mutex;
use ed25519_dalek::PublicKey as DalekPublicKey;
use std::sync::Arc;
/// ForeignAPI Middleware Check callback
@ -450,6 +453,43 @@ where
post_automatically,
)
}
// Below is a foreign wrapper around owner calls to 'new' and 'sign' which are only executed
// if this is a receiving contract. This preserves the ability to receive on a foreign interface.
/// TODO
pub fn contract_new(
&self,
keychain_mask: Option<&SecretKey>,
args: &ContractNewArgsAPI,
) -> Result<Slate, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
// TODO: self.doctest_mode ?
foreign::contract_new(&mut **w, keychain_mask, &args)
}
/// TODO
pub fn contract_sign(
&self,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
args: &ContractSetupArgsAPI,
) -> Result<Slate, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
foreign::contract_sign(&mut **w, keychain_mask, &args, &slate)
}
/// TODO
pub fn verify_payment_proof_invoice(
&self,
recipient_address: &DalekPublicKey,
proof: &InvoiceProof,
) -> Result<(), Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
foreign::verify_payment_proof_invoice(&mut **w, recipient_address, proof)
}
}
#[doc(hidden)]

View file

@ -52,6 +52,7 @@ pub trait ForeignRpc {
"Ok": {
"foreign_api_version": 2,
"supported_slate_versions": [
"V5",
"V4"
]
}
@ -141,7 +142,7 @@ pub trait ForeignRpc {
}
],
"sta": "S1",
"ver": "4:2"
"ver": "5:2"
},
null,
null
@ -176,7 +177,7 @@ pub trait ForeignRpc {
}
],
"sta": "S2",
"ver": "4:2"
"ver": "5:2"
}
}
}
@ -205,7 +206,7 @@ pub trait ForeignRpc {
"method": "finalize_tx",
"id": 1,
"params": [{
"ver": "4:2",
"ver": "5:2",
"id": "0436430c-2b02-624c-2032-570501212b00",
"sta": "I2",
"off": "383bc9df0dd332629520a0a72f8dd7f0e97d579dccb4dbdc8592aa3d424c846c",
@ -275,7 +276,7 @@ pub trait ForeignRpc {
}
],
"sta": "I3",
"ver": "4:2"
"ver": "5:2"
}
}
}

View file

@ -16,6 +16,7 @@
use chrono::prelude::*;
use ed25519_dalek::SecretKey as DalekSecretKey;
use grin_wallet_libwallet::contract::proofs::InvoiceProof;
use grin_wallet_libwallet::RetrieveTxQueryArgs;
use uuid::Uuid;
@ -27,6 +28,10 @@ use crate::impls::SlateSender as _;
use crate::keychain::{Identifier, Keychain};
use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage};
use crate::libwallet::api_impl::{owner, owner_updater};
use crate::libwallet::contract::types::{
ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI,
};
use crate::libwallet::mwmixnet::types::{MixnetReqCreationParams, SwapReq};
use crate::libwallet::{
AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient,
NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress,
@ -766,6 +771,78 @@ where
owner::issue_invoice_tx(&mut **w, keychain_mask, args, self.doctest_mode)
}
/// TODO
pub fn contract_new(
&self,
keychain_mask: Option<&SecretKey>,
args: &ContractNewArgsAPI,
) -> Result<Slate, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
// TODO: self.doctest_mode ?
owner::contract_new(&mut **w, keychain_mask, &args)
}
// /// TODO
// pub fn contract_setup(
// &self,
// keychain_mask: Option<&SecretKey>,
// slate: &Slate,
// args: &ContractSetupArgsAPI,
// ) -> Result<Slate, Error> {
// let mut w_lock = self.wallet_inst.lock();
// let w = w_lock.lc_provider()?.wallet_inst()?;
// // TODO: self.doctest_mode ?
// owner::contract_setup(&mut **w, keychain_mask, &args, &slate)
// }
/// TODO
pub fn contract_sign(
&self,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
args: &ContractSetupArgsAPI,
) -> Result<Slate, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
owner::contract_sign(&mut **w, keychain_mask, &args, &slate)
}
/// TODO
pub fn get_slate_index_matching_my_context(
&self,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
) -> Result<usize, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
owner::get_slate_index_matching_my_context(&mut **w, keychain_mask, &slate)
}
/// TODO
pub fn contract_revoke(
&self,
keychain_mask: Option<&SecretKey>,
args: &ContractRevokeArgsAPI,
) -> Result<Option<Slate>, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
owner::contract_revoke(&mut **w, keychain_mask, &args)
}
/// Create MXMixnet request
pub fn create_mwmixnet_req(
&self,
keychain_mask: Option<&SecretKey>,
params: &MixnetReqCreationParams,
slate: &Slate,
// use_test_rng: bool,
) -> Result<SwapReq, Error> {
let mut w_lock = self.wallet_inst.lock();
let w = w_lock.lc_provider()?.wallet_inst()?;
owner::create_mwmixnet_req(&mut **w, keychain_mask, params, slate)
}
/// Processes an invoice tranaction created by another party, essentially
/// a `request for payment`. The incoming slate should contain a requested
/// amount, an output created by the invoicer convering the amount, and
@ -2355,6 +2432,32 @@ where
)
}
/// TODO: Temporary, likely should merge with above
pub fn retrieve_payment_proof_invoice(
&self,
keychain_mask: Option<&SecretKey>,
refresh_from_node: bool,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
) -> Result<InvoiceProof, Error> {
let tx = {
let t = self.status_tx.lock();
t.clone()
};
let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) {
true => false,
false => refresh_from_node,
};
owner::retrieve_payment_proof_invoice(
self.wallet_inst.clone(),
keychain_mask,
&tx,
refresh_from_node,
tx_id,
tx_slate_id,
)
}
/// Verifies a [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html)
/// This process entails:
///

View file

@ -20,6 +20,9 @@ use crate::config::{TorConfig, WalletConfig};
use crate::core::core::OutputFeatures;
use crate::core::global;
use crate::keychain::{Identifier, Keychain};
use crate::libwallet::contract::types::{
ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI,
};
use crate::libwallet::{
AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient,
NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack,
@ -501,7 +504,7 @@ pub trait OwnerRpc {
}
],
"sta": "S1",
"ver": "4:2"
"ver": "5:2"
}
}
}
@ -510,8 +513,49 @@ pub trait OwnerRpc {
```
*/
/**
TODO: Full docs once API has stabilised
*/
fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result<VersionedSlate, Error>;
/**
TODO: Implement
*/
// fn contract_setup(
// &self,
// token: Token,
// slate: VersionedSlate,
// args: ContractSetupArgsAPI,
// ) -> Result<VersionedSlate, Error>;
/**
TODO: Full docs once API has stabilised
*/
fn contract_new(&self, token: Token, args: ContractNewArgsAPI)
-> Result<VersionedSlate, Error>;
/**
TODO: Full docs once API has stabilised
*/
fn contract_sign(
&self,
token: Token,
slate: VersionedSlate,
args: ContractSetupArgsAPI,
) -> Result<VersionedSlate, Error>;
/**
TODO: Full docs once API has stabilised
*/
fn contract_revoke(
&self,
token: Token,
args: ContractRevokeArgsAPI,
) -> Result<Option<VersionedSlate>, Error>;
/**
;Networked version of [Owner::issue_invoice_tx](struct.Owner.html#method.issue_invoice_tx).
@ -548,7 +592,7 @@ pub trait OwnerRpc {
}
],
"sta": "I1",
"ver": "4:2"
"ver": "5:2"
}
}
}
@ -919,7 +963,7 @@ pub trait OwnerRpc {
"id": "0436430c-2b02-624c-2032-570501212b00",
"sigs": [],
"sta": "S3",
"ver": "4:3"
"ver": "5:3"
}
}
}
@ -2052,6 +2096,65 @@ where
VersionedSlate::into_version(slate, version)
}
fn contract_new(
&self,
token: Token,
args: ContractNewArgsAPI,
) -> Result<VersionedSlate, Error> {
let slate = Owner::contract_new(self, (&token.keychain_mask).as_ref(), &args)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(slate, version)
}
// fn contract_setup(
// &self,
// token: Token,
// in_slate: VersionedSlate,
// args: ContractSetupArgsAPI,
// ) -> Result<VersionedSlate, Error> {
// let slate = Owner::contract_setup(
// self,
// (&token.keychain_mask).as_ref(),
// &Slate::from(in_slate),
// &args,
// )?;
// let version = SlateVersion::V4;
// VersionedSlate::into_version(slate, version)
// }
fn contract_sign(
&self,
token: Token,
in_slate: VersionedSlate,
args: ContractSetupArgsAPI,
) -> Result<VersionedSlate, Error> {
let slate = Owner::contract_sign(
self,
(&token.keychain_mask).as_ref(),
&Slate::from(in_slate),
&args,
)?;
let version = SlateVersion::V4;
VersionedSlate::into_version(slate, version)
}
fn contract_revoke(
&self,
token: Token,
args: ContractRevokeArgsAPI,
) -> Result<Option<VersionedSlate>, Error> {
let slate_opt = Owner::contract_revoke(self, (&token.keychain_mask).as_ref(), &args)?;
let version = SlateVersion::V4;
// We return a slate only when we had to perform a self-spend safe cancel
if slate_opt.is_some() {
return Ok(Some(VersionedSlate::into_version(
slate_opt.unwrap(),
version,
)?));
}
Ok(None)
}
fn issue_invoice_tx(
&self,
token: Token,

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_config"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Configuration for grin wallet , a simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
license = "Apache-2.0"
@ -16,13 +16,13 @@ serde_derive = "1"
toml = "0.5"
dirs = "2.0"
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "../util", version = "5.4.0-contracts.0" }
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# For beta release

View file

@ -28,6 +28,9 @@ pub struct WalletConfig {
pub chain_type: Option<ChainTypes>,
/// The port this wallet will run on
pub api_listen_port: u16,
/// Listen interface for the owner API, should be hidden from config by default
#[serde(skip_serializing)]
pub owner_api_listen_interface: Option<String>,
/// The port this wallet's owner API will run on
pub owner_api_listen_port: Option<u16>,
/// Location of the secret for basic auth on the Owner API
@ -61,6 +64,7 @@ impl Default for WalletConfig {
WalletConfig {
chain_type: Some(ChainTypes::Mainnet),
api_listen_port: 3415,
owner_api_listen_interface: Some(WalletConfig::default_owner_api_listen_interface()),
owner_api_listen_port: Some(WalletConfig::default_owner_api_listen_port()),
api_secret_path: Some(".owner_api_secret".to_string()),
node_api_secret_path: Some(".foreign_api_secret".to_string()),
@ -82,6 +86,11 @@ impl WalletConfig {
format!("127.0.0.1:{}", self.api_listen_port)
}
/// Default listener port
pub fn default_owner_api_listen_interface() -> String {
"127.0.0.1".to_string()
}
/// Default listener port
pub fn default_owner_api_listen_port() -> u16 {
3420
@ -92,6 +101,13 @@ impl WalletConfig {
500_000
}
/// Use value from config file, defaulting to sensible value if missing.
pub fn owner_api_listen_interface(&self) -> String {
self.owner_api_listen_interface
.clone()
.unwrap_or_else(|| WalletConfig::default_owner_api_listen_interface())
}
/// Use value from config file, defaulting to sensible value if missing.
pub fn owner_api_listen_port(&self) -> u16 {
self.owner_api_listen_port
@ -100,7 +116,11 @@ impl WalletConfig {
/// Owner API listen address
pub fn owner_api_listen_addr(&self) -> String {
format!("127.0.0.1:{}", self.owner_api_listen_port())
format!(
"{}:{}",
self.owner_api_listen_interface(),
self.owner_api_listen_port()
)
}
/// Accept fee base

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_controller"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Controllers for grin wallet instantiation"
license = "Apache-2.0"
@ -30,19 +30,19 @@ lazy_static = "1"
thiserror = "1"
qr_code = "1.1.0"
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_api = { path = "../api", version = "5.4.0-alpha.1" }
grin_wallet_impls = { path = "../impls", version = "5.4.0-alpha.1" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "../util", version = "5.4.0-contracts.0" }
grin_wallet_api = { path = "../api", version = "5.4.0-contracts.0" }
grin_wallet_impls = { path = "../impls", version = "5.4.0-contracts.0" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-contracts.0" }
grin_wallet_config = { path = "../config", version = "5.4.0-contracts.0" }
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_keychain = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_api = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_keychain = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# grin_api = "5.4.0-contracts.0"
# For beta release
@ -72,7 +72,7 @@ remove_dir_all = "0.7"
##### Grin Imports
# For Release
# grin_chain = "5.4.0-alpha.1"
# grin_chain = "5.4.0-contracts.0"
# For beta release

View file

@ -13,7 +13,6 @@
// limitations under the License.
//! Grin wallet command-line function implementations
use crate::api::TLSConfig;
use crate::apiwallet::{try_slatepack_sync_workflow, Owner};
use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME};
@ -22,6 +21,11 @@ use crate::error::Error;
use crate::impls::PathToSlatepack;
use crate::impls::SlateGetter as _;
use crate::keychain;
use crate::libwallet::api_impl::owner;
use crate::libwallet::contract::can_finalize;
use crate::libwallet::contract::types::{
ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs,
};
use crate::libwallet::{
self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack,
SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider,
@ -31,9 +35,12 @@ use crate::util::{Mutex, ZeroingString};
use crate::{controller, display};
use ::core::time;
use qr_code::QrCode;
use serde::{Deserialize, Serialize};
use serde_json as json;
use std::convert::TryFrom;
use std::fmt;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use std::sync::atomic::Ordering;
use std::sync::Arc;
@ -558,6 +565,140 @@ where
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SlatepackOut {
/// Is slatepack encrypted
pub is_encrypted: bool,
/// Is slatepack finalized
pub is_finalized: bool,
/// File where slatepack is saved
pub out_file: String,
/// Slatepack message. Encrypted or not.
pub message: String,
}
impl fmt::Display for SlatepackOut {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let start_meta = "--------------- SLATEPACK METADATA --------------";
let meta = format!(
"Slate encrypted: {}\nSlate finalized: {}\nSlate saved to file: {}",
self.is_encrypted, self.is_finalized, self.out_file
);
let start_slatepack = "-------------- CUT BELOW THIS LINE --------------";
let end_slatepack = "-------------- CUT ABOVE THIS LINE --------------";
write!(
f,
"{start_meta}\n\n{meta}\n\n{start_slatepack}\n\n{}\n\n{end_slatepack}",
self.message
)
}
}
impl SlatepackOut {
fn as_json(&self) -> String {
serde_json::to_string_pretty(&self).unwrap()
}
pub fn print(&self, as_json: bool) -> () {
if !self.is_finalized {
if as_json {
println!("{}", self.as_json());
} else {
println!("{}", self);
}
} else {
println!("Transaction was broadcasted."); // TODO: as_json makes no sense here, fix later.
}
}
}
pub fn print_slatepack<L, C, K>(
api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
counterparty_addr: &str,
out_file: Option<String>,
as_json: bool,
) -> ()
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
// For now, we don't compact slates with sl.compact(). We first make them work without compaction.
let slate_out =
prepare_slatepack(api, keychain_mask, &slate, &counterparty_addr, out_file).unwrap();
slate_out.print(as_json);
}
pub fn prepare_slatepack<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
dest: &str,
out_file_override: Option<String>,
) -> Result<SlatepackOut, libwallet::Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
// Same as output_slatepack except that we don't write to stdout, care about locking or whether the slate was finalized.
// Output the slatepack file to stdout and to a file
let mut message = String::from("");
let mut address = None;
let mut tld = String::from("");
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
address = match SlatepackAddress::try_from(dest) {
Ok(a) => Some(a),
Err(_) => None,
};
// encrypt for recipient by default
let recipients = match address.clone() {
Some(a) => vec![a],
None => vec![],
};
// TODO: what is sender_index?
message = api.create_slatepack_message(m, &slate, Some(0), recipients)?;
// Trim the \n at the end.
let len_withoutcrlf = message.trim_end().len();
message.truncate(len_withoutcrlf);
tld = api.get_top_level_directory()?;
Ok(())
})?;
// create a directory to which files will be output
let slate_dir = format!("{}/{}", tld, "slatepack");
let _ = std::fs::create_dir_all(slate_dir.clone());
let out_file_name = match out_file_override {
None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state),
Some(f) => f,
};
let mut output = File::create(out_file_name.clone())?;
output.write_all(&message.as_bytes())?;
output.sync_all()?;
// Since we always finalize if we can, we can also use this to know if the tx is finalized
let is_finalized = can_finalize(slate);
let slate_out = SlatepackOut {
is_encrypted: address.is_some(),
is_finalized: is_finalized,
out_file: out_file_name,
message: message,
};
// TODO: We save the slatepack, but it is encrypted for the counterparty. It seems hard to
// know which slatepack is which if we can't decrypt them. Either add some more metadata
// to slatepacks e.g. timestamp, counterparty address or save also a version that is encrypted with
// our own address so we can view it.
Ok(slate_out)
}
// Parse a slate and slatepack from a message
pub fn parse_slatepack<L, C, K>(
owner_api: &mut Owner<L, C, K>,
@ -1474,3 +1615,341 @@ where
})?;
Ok(())
}
/// Create new contract command arguments
#[derive(Clone)]
pub struct ContractNewArgs {
/// Address of the counterparty
pub counterparty_addr: String,
/// Receive amount
pub receive: Option<u64>,
/// Send amount
pub send: Option<u64>,
/// The human readable account name from which to draw outputs
/// for the transaction, overriding whatever the active account is as set via the
/// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method.
pub src_acct_name: Option<String>,
/// Number of participants in a contract (either 1 or 2)
pub num_participants: u8,
/// Show the resulting slatepack as JSON
pub as_json: bool,
/// Use the specified inputs (comma separated input commitments)
pub use_inputs: Option<String>,
/// How to separate outputs (command separated amounts)
pub make_outputs: Option<String>,
// Future features
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
/// Select outputs early
pub add_outputs: bool,
}
impl ContractNewArgs {
fn get_net_change(&self) -> i64 {
// TODO: could the cast 'as i64' overflow or something?
match self.receive {
None => match self.send {
None => panic!("Send or receive not specified."),
Some(v) => -(v as i64), // negative net change on send
},
Some(v) => v as i64, // positive net change on receive
}
}
// Create a ContractNewArgsAPI from the ContractNewArgs
fn to_api_args(&self) -> ContractNewArgsAPI {
let net_change = self.get_net_change();
ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
src_acct_name: match self.src_acct_name.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
},
net_change: Some(net_change),
num_participants: self.num_participants,
add_outputs: self.add_outputs,
selection_args: OutputSelectionArgs {
use_inputs: match self.use_inputs.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
},
make_outputs: match self.make_outputs.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
},
..Default::default()
},
proof_args: Default::default(),
},
..Default::default()
}
}
}
pub fn contract_new<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
args: ContractNewArgs,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K>,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
let contract_new_args = args.to_api_args();
let slate = api.contract_new(m, &contract_new_args)?;
print_slatepack(
api,
keychain_mask,
&slate,
&args.counterparty_addr,
args.outfile,
args.as_json,
);
Ok(())
})?;
Ok(())
}
/// Sign contract command argument
#[derive(Clone)]
pub struct ContractSetupArgs {
/// Address of the counterparty
pub counterparty_addr: Option<String>,
/// Receive amount
pub receive: Option<u64>,
/// Send amount
pub send: Option<u64>,
/// Show the resulting slatepack as JSON
pub as_json: bool,
/// Use the specified inputs (comma separated input commitments)
pub use_inputs: Option<String>,
/// How to separate outputs (command separated amounts)
pub make_outputs: Option<String>,
// Future features
/// Whether we should automatically sign a receive of any value
// pub auto_receive: Option<bool>,
/// Custom fee contribution
pub fee_rate: Option<u32>,
/// Save slatepack to a specific filename
pub outfile: Option<String>,
/// Add outputs
pub add_outputs: bool, // lock outputs early
}
impl ContractSetupArgs {
fn get_net_change(&self) -> Option<i64> {
let mut net_change: Option<i64> = None;
// TODO: Check bounds before casting to i64.
if self.receive.is_some() && self.send.is_some() {
panic!("Can't pass both --receive and --send parameters.");
}
if self.receive.is_some() {
net_change = Some(self.receive.unwrap() as i64);
}
if self.send.is_some() {
net_change = Some(-(self.send.unwrap() as i64));
}
net_change
}
// Create a ContractSetupArgsAPI from the ContractSetupArgs
fn to_api_args(&self) -> ContractSetupArgsAPI {
let net_change = self.get_net_change();
ContractSetupArgsAPI {
// TODO: num_participants is derived here. It should be an Option and read from the slate.
// Need to check no attack are possible regarding kernel fee contribution.
net_change: net_change,
add_outputs: self.add_outputs,
selection_args: OutputSelectionArgs {
use_inputs: match self.use_inputs.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
},
make_outputs: match self.make_outputs.as_ref() {
Some(v) => Some(v.to_string()),
None => None,
},
..Default::default()
},
..Default::default()
}
}
}
// pub fn contract_setup<L, C, K>(
// owner_api: &mut Owner<L, C, K>,
// keychain_mask: Option<&SecretKey>,
// args: ContractSetupArgs,
// ) -> Result<(), Error>
// where
// L: WalletLCProvider<'static, C, K>,
// C: NodeClient + 'static,
// K: keychain::Keychain + 'static,
// {
// controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
// // Read the slatepack from stdin
// println!("Paste slatepack:");
// let mut slatepack_msg = String::new();
// io::stdin()
// .read_line(&mut slatepack_msg)
// .expect("Failed to read from stdin");
// let mut contract_setup_args = args.to_api_args();
// // Decrypt the slate, perform setup on it and encrypt it for the next party
// // TODO: Make sure you get the counterparty_addr and slate with 1 call.
// let slatepack = owner::decode_slatepack_message(
// api.wallet_inst.clone(),
// keychain_mask,
// String::from(slatepack_msg.clone()),
// vec![0],
// )?;
// let counterparty_addr =
// if args.counterparty_addr.is_some() {
// args.counterparty_addr.unwrap()
// } else {
// if !slatepack.sender.is_some() {
// panic!("No address to encrypt for. Contracts only support encrypted slates right now.");
// }
// String::try_from(&slatepack.sender.unwrap())?
// };
// let mut slate = owner::slate_from_slatepack_message(
// api.wallet_inst.clone(),
// keychain_mask,
// String::from(slatepack_msg),
// vec![0],
// )?;
// // We read the number of participants from the slate that was already created. We need this to
// // avoid taking the default value of 2 in case of 3-party computation and thus incorrectly computing
// // our kernel cost contribution.
// contract_setup_args.num_participants = slate.num_participants;
// slate = api.contract_setup(m, &slate, &contract_setup_args)?;
// print_slatepack(
// api,
// keychain_mask,
// &slate,
// &counterparty_addr,
// args.outfile,
// args.as_json,
// );
// Ok(())
// })?;
// Ok(())
// }
pub fn contract_sign<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
args: ContractSetupArgs,
broadcast_tx: bool,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K>,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
// Read the slatepack from stdin
println!("Paste slatepack:");
let mut slatepack_msg = String::new();
io::stdin()
.read_line(&mut slatepack_msg)
.expect("Failed to read from stdin");
// Args for signing are just setup args
let contract_sign_args = args.to_api_args();
// Decrypt the slate, sign it and encrypt it for the next party
// TODO: Make sure you get the counterparty_addr and slate with 1 call.
let slatepack = owner::decode_slatepack_message(
api.wallet_inst.clone(),
keychain_mask,
String::from(slatepack_msg.clone()),
vec![0],
)?;
let counterparty_addr =
if args.counterparty_addr.is_some() {
args.counterparty_addr.unwrap()
} else {
if !slatepack.sender.is_some() {
panic!("No address to encrypt for. Contracts only support encrypted slates right now.");
}
String::try_from(&slatepack.sender.unwrap())?
};
let mut slate = owner::slate_from_slatepack_message(
api.wallet_inst.clone(),
keychain_mask,
String::from(slatepack_msg),
vec![0],
)?;
slate = api.contract_sign(m, &slate, &contract_sign_args)?;
print_slatepack(
api,
keychain_mask,
&slate,
&counterparty_addr,
args.outfile,
args.as_json,
);
if broadcast_tx {
let is_finalized = can_finalize(&slate);
if is_finalized {
api.post_tx(keychain_mask, &slate, true)?;
}
}
Ok(())
})?;
Ok(())
}
#[derive(Clone)]
pub struct ContractRevokeArgs {
/// Id of a transaction we want to cancel
pub tx_id: u32,
}
pub fn contract_revoke<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Option<&SecretKey>,
args: ContractRevokeArgs,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K>,
C: NodeClient + 'static,
K: keychain::Keychain + 'static,
{
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
let slate_opt = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id: args.tx_id })?;
// TODO: replace dest="nope" with our own address and add --as-json support
if slate_opt.is_some() {
let slate_out =
prepare_slatepack(api, keychain_mask, &slate_opt.unwrap(), "nope", None)?;
println!("{}", slate_out);
}
Ok(())
})?;
Ok(())
}

View file

@ -15,18 +15,24 @@
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate grin_wallet_libwallet as libwallet;
extern crate log;
use grin_chain as chain;
use grin_core as core;
use grin_keychain as keychain;
use grin_util as util;
use self::core::global;
use self::core::global::ChainTypes;
use self::keychain::ExtKeychain;
use self::keychain::{ExtKeychain, Keychain};
use self::libwallet::WalletInst;
use impls::test_framework::{LocalWalletClient, WalletProxy};
use chain::Chain;
use grin_wallet_controller::Error;
use impls::test_framework::{self, LocalWalletClient, WalletProxy};
use impls::{DefaultLCProvider, DefaultWalletImpl};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::thread;
use util::secp::key::SecretKey;
use util::{Mutex, ZeroingString};
@ -176,3 +182,133 @@ pub fn open_local_wallet(
.unwrap();
(Arc::new(Mutex::new(wallet)), mask)
}
// Creates the given number of wallets and spawns a thread that runs the wallet proxy
#[allow(dead_code)]
pub fn create_wallets(
wallets_def: Vec<Vec<(&'static str, u64)>>, // a vector of boolean that represent whether we mine into a wallet
test_dir: &'static str,
) -> Result<
(
Vec<(
Arc<
Mutex<
Box<
dyn WalletInst<
'static,
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
LocalWalletClient,
ExtKeychain,
>,
>,
>,
>,
Option<SecretKey>,
)>, // wallets
Arc<Chain>, // chain
Arc<AtomicBool>, // stopper
u64, // block height
),
Error,
> {
// 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();
let stopper = wallet_proxy.running.clone();
let mut wallets = vec![];
for i in 0..wallets_def.len() {
let name = format!("wallet{}", i + 1);
let wclient = LocalWalletClient::new(&name, wallet_proxy.tx.clone());
let (wallet1, mask1) = create_local_wallet(
test_dir,
&name,
None,
// $seed_phrase.clone(),
wclient.clone(),
true,
);
wallet_proxy.add_wallet(
&name,
wclient.get_send_instance(),
wallet1.clone(),
mask1.clone(),
);
wallets.push((wallet1, mask1));
}
// Set the wallet proxy listener running
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
log::error!("Wallet Proxy error: {}", e);
}
});
// // Mine values into wallets
// // few values to keep things shorter
let reward = core::consensus::REWARD;
let mut bh = 0u64;
for (idx, accs) in wallets_def.iter().enumerate() {
let wallet1 = wallets[idx].0.clone();
let mask1 = wallets[idx].1.as_ref();
for (acc_idx, (acc_name, num_mined_blocks)) in accs.iter().enumerate() {
// create the account
if acc_name.to_string() != "default" {
wallet::controller::owner_single_use(
Some(wallet1.clone()),
mask1,
None,
|api, m| {
let new_path = api.create_account_path(m, acc_name)?;
assert_eq!(
new_path,
ExtKeychain::derive_key_id(2, acc_idx as u32, 0, 0, 0) // NOTE: default should always be at 0 and is already created
);
Ok(())
},
)?;
}
// Get some mining done
if *num_mined_blocks == 0 {
continue;
}
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name(acc_name)?;
}
let _ = test_framework::award_blocks_to_wallet(
&chain,
wallet1.clone(),
mask1,
*num_mined_blocks as usize,
false,
);
bh += num_mined_blocks;
// Sanity check wallet 1 contents
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, num_mined_blocks * reward);
Ok(())
})?;
}
// Sanity check the number of accounts
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let accounts = api.accounts(m)?;
assert_eq!(accounts.len(), accs.len());
Ok(())
})?;
// Set the account on the wallet to "default"
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("default")?;
}
}
Ok((wallets, chain, stopper, bh))
}

View file

@ -0,0 +1,310 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test contract utils
#[macro_use]
extern crate log;
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
use grin_wallet_libwallet as libwallet;
use grin_wallet_util::grin_core as core;
use grin_wallet_util::grin_keychain as keychain;
use grin_wallet_util::grin_util as util;
use self::keychain::ExtKeychain;
use self::libwallet::WalletInst;
// use impls::test_framework::{LocalWalletClient, WalletProxy};
use crate::chain::Chain;
use grin_wallet_util::grin_chain as chain;
use impls::{DefaultLCProvider, DefaultWalletImpl};
use std::sync::Arc;
use util::secp::key::SecretKey;
use util::{Mutex, ZeroingString};
use impls::test_framework::{self, LocalWalletClient, WalletProxy};
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState};
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
// #[macro_use]
mod common;
use common::{clean_output_dir, create_local_wallet, create_wallet_proxy, setup};
pub fn create_wallets(
test_dir: &'static str,
) -> (
Vec<(
Arc<
Mutex<
Box<
dyn WalletInst<
'static,
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
LocalWalletClient,
ExtKeychain,
>,
>,
>,
>,
Option<SecretKey>,
)>,
Arc<Chain>, // chain
Arc<AtomicBool>, // stopper
) {
// Create a new proxy to simulate server and wallet responses
let mut wallet_proxy = common::create_wallet_proxy(test_dir);
let chain = wallet_proxy.chain.clone();
let stopper = wallet_proxy.running.clone();
let mut wallets = vec![];
for i in 0..2 {
let name = format!("wallet{}", i + 1);
let wclient = LocalWalletClient::new(&name, wallet_proxy.tx.clone());
let (wallet1, mask1) = common::create_local_wallet(
test_dir,
&name,
None,
// $seed_phrase.clone(),
wclient.clone(),
true,
);
wallet_proxy.add_wallet(
&name,
wclient.get_send_instance(),
wallet1.clone(),
mask1.clone(),
);
// create_wallet_and_add!(
// client1,
// wallet1,
// mask1_i,
// test_dir,
// "wallet1",
// None,
// &mut wallet_proxy,
// true
// );
wallets.push((wallet1, mask1));
}
// Set the wallet proxy listener running
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
}
});
(wallets, chain, stopper)
// let $client = LocalWalletClient::new($name, $proxy.tx.clone());
// let ($wallet, $mask) = common::create_local_wallet(
// $test_dir,
// $name,
// $seed_phrase.clone(),
// $client.clone(),
// $create_mask,
// );
// $proxy.add_wallet(
// $name,
// $client.get_send_instance(),
// $wallet.clone(),
// $mask.clone(),
// );
}
// #[macro_export]
// macro_rules! create_wallets {
// ($client:ident, $wallet: ident, $mask: ident, $test_dir: expr, $name: expr, $seed_phrase: expr, $proxy: expr, $create_mask: expr) => {
// // 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();
// let stopper = wallet_proxy.running.clone();
// let rv = vec![];
// for i in 0..wallets.len() {
// let name = format!("wallet{}", i + 1);
// let wclient = LocalWalletClient::new(name, wallet_proxy.tx.clone());
// let (wallet1, mask1) = common::create_local_wallet(
// $test_dir,
// name,
// None,
// // $seed_phrase.clone(),
// wallet_proxy.clone(),
// true,
// );
// wallet_proxy.add_wallet(
// name,
// wclient.get_send_instance(),
// wallet1.clone(),
// mask1.clone(),
// );
// // create_wallet_and_add!(
// // client1,
// // wallet1,
// // mask1_i,
// // test_dir,
// // "wallet1",
// // None,
// // &mut wallet_proxy,
// // true
// // );
// rv.push((wallet1, mask1));
// }
// rv
// // let $client = LocalWalletClient::new($name, $proxy.tx.clone());
// // let ($wallet, $mask) = common::create_local_wallet(
// // $test_dir,
// // $name,
// // $seed_phrase.clone(),
// // $client.clone(),
// // $create_mask,
// // );
// // $proxy.add_wallet(
// // $name,
// // $client.get_send_instance(),
// // $wallet.clone(),
// // $mask.clone(),
// // );
// };
// }
// prepare wallets
// fn create_wallets(
// wallets: Vec<u64>,
// test_dir: &'static str,
// // wallet_proxy: WalletProxy<
// // 'static,
// // DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
// // LocalWalletClient,
// // ExtKeychain,
// // >,
// ) -> Vec<(
// Arc<
// Mutex<
// Box<
// dyn WalletInst<
// 'static,
// DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
// LocalWalletClient,
// ExtKeychain,
// >,
// >,
// >,
// >,
// Option<SecretKey>,
// )> {
// // 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();
// let stopper = wallet_proxy.running.clone();
// let rv = vec![];
// for _ in 0..wallets.len() {
// create_wallet_and_add!(
// client1,
// wallet1,
// mask1_i,
// test_dir,
// "wallet1",
// None,
// &mut wallet_proxy,
// true
// );
// rv.push((wallet1, mask1_i));
// }
// rv
// }
// /// prepare two wallets for testing
// fn prepare_wallets(n_wallets: u8, test_dir: &'static str) -> Result<Vec<()>, libwallet::Error> {
// // 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();
// let stopper = wallet_proxy.running.clone();
// create_wallet_and_add!(
// client1,
// wallet1,
// mask1_i,
// test_dir,
// "wallet1",
// None,
// &mut wallet_proxy,
// true
// );
// let mask1 = (&mask1_i).as_ref();
// create_wallet_and_add!(
// client2,
// wallet2,
// mask2_i,
// test_dir,
// "wallet2",
// None,
// &mut wallet_proxy,
// true
// );
// let mask2 = (&mask2_i).as_ref();
// // Set the wallet proxy listener running
// thread::spawn(move || {
// if let Err(e) = wallet_proxy.run() {
// error!("Wallet Proxy error: {}", e);
// }
// });
// // few values to keep things shorter
// let reward = core::consensus::REWARD;
// // add some accounts
// wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// api.create_account_path(m, "mining")?;
// api.create_account_path(m, "listener")?;
// Ok(())
// })?;
// // Get some mining done
// {
// wallet_inst!(wallet1, w);
// w.set_parent_key_id_by_name("mining")?;
// }
// let mut bh = 10u64;
// let _ =
// test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
// // Sanity check wallet 1 contents
// wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
// assert!(wallet1_refreshed);
// assert_eq!(wallet1_info.last_confirmed_height, bh);
// assert_eq!(wallet1_info.total, bh * reward);
// Ok(())
// })?;
// // let logging finish
// stopper.store(false, Ordering::Relaxed);
// thread::sleep(Duration::from_millis(200));
// Ok(())
// }
// #[test]
// fn wallet_contract_rsr_tx() -> Result<(), libwallet::Error> {
// let test_dir = "test_output/contract_rsr_tx";
// setup(test_dir);
// contract_rsr_tx_impl(test_dir)?;
// clean_output_dir(test_dir);
// Ok(())
// }

View file

@ -0,0 +1,259 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contracts with different accounts
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_core as core;
use grin_keychain as keychain;
use self::core::global;
use self::keychain::{ExtKeychain, Keychain};
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract accounts testing (mostly the same as accounts.rs)
fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets with some extra accounts and don't mine anything in them
let (wallets, chain, stopper, _bh) = create_wallets(
vec![
vec![
("default", 0),
("account1", 0),
("account2", 0),
("account3", 0),
],
vec![("default", 0), ("listener_account", 0)],
],
test_dir,
)
.unwrap();
let wallet1 = wallets[0].0.clone();
let mask1 = wallets[0].1.as_ref();
let wallet2 = wallets[1].0.clone();
let mask2 = wallets[1].1.as_ref();
// few values to keep things shorter
let reward = core::consensus::REWARD;
let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height
// Default wallet 2 to listen on that account
{
wallet_inst!(wallet2, w);
w.set_parent_key_id_by_name("listener_account")?;
}
// Mine into two different accounts in the same wallet
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0));
}
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 7, false);
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account2")?;
assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0));
}
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false);
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 12);
assert_eq!(wallet1_info.total, 5 * reward);
assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward);
// check tx log as well
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 5);
Ok(())
})?;
// now check second account
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// check last confirmed height on this account is different from above (should be 0)
let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?;
assert_eq!(wallet1_info.last_confirmed_height, 0);
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 12);
assert_eq!(wallet1_info.total, 7 * reward);
assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward);
// check tx log as well
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 7);
Ok(())
})?;
// should be nothing in default account
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("default")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?;
assert_eq!(wallet1_info.last_confirmed_height, 0);
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 12);
assert_eq!(wallet1_info.total, 0,);
assert_eq!(wallet1_info.amount_currently_spendable, 0,);
// check tx log as well
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 0);
Ok(())
})?;
// TODO: check what send_tx_slate_direct call does in accounts.rs test
// TODO: check that you can't call send on the default account because you have no funds
// Send a tx from wallet1::account1 -> wallet2::listener_account
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
}
let mut slate = Slate::blank(0, true); // this gets overriden below
let mut sender_address = None;
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// Send wallet inititates a standard transaction with --send=5
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
sender_address = Some(api.get_slatepack_address(mask1, 0)?.pub_key);
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
let mut recipient_address = None;
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
// Receive wallet calls --receive=5
let args = &mut ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
};
args.proof_args.sender_address = sender_address;
slate = api.contract_sign(m, &slate, args)?;
recipient_address = Some(api.get_slatepack_address(mask2, 0)?.pub_key);
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard2);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard3);
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 13);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 9);
Ok(())
})?;
// other account should be untouched
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account2")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?;
assert_eq!(wallet1_info.last_confirmed_height, 12);
let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert_eq!(wallet1_info.last_confirmed_height, 13);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
println!("{:?}", txs);
assert_eq!(txs.len(), 5);
Ok(())
})?;
// wallet 2 should only have this tx on the listener account
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet2_refreshed);
assert_eq!(wallet2_info.last_confirmed_height, 13);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 1);
Ok(())
})?;
// Default account on wallet 2 should be untouched
{
wallet_inst!(wallet2, w);
w.set_parent_key_id_by_name("default")?;
}
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
let (_, wallet2_info) = api.retrieve_summary_info(m, false, 1)?;
assert_eq!(wallet2_info.last_confirmed_height, 0);
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet2_refreshed);
assert_eq!(wallet2_info.last_confirmed_height, 13);
assert_eq!(wallet2_info.total, 0,);
assert_eq!(wallet2_info.amount_currently_spendable, 0,);
// check tx log as well
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 0);
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_accounts() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_accounts";
setup(test_dir);
contract_accounts_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,212 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contracts with different accounts and switching between them
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_core as core;
use grin_wallet_libwallet as libwallet;
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract accounts testing when switching between accounts during transaction building
fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets with some extra accounts and don't mine anything in them
let (wallets, _chain, stopper, _bh) = create_wallets(
vec![
vec![("default", 0), ("account1", 1), ("account2", 2)],
vec![("default", 0), ("account1", 3), ("account2", 4)],
],
test_dir,
)
.unwrap();
let wallet1 = wallets[0].0.clone();
let mask1 = wallets[0].1.as_ref();
let wallet2 = wallets[1].0.clone();
let mask2 = wallets[1].1.as_ref();
let reward = core::consensus::REWARD;
// wallet1::account1 should have 1 in account1 (1 spendable)
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 10);
assert_eq!(wallet1_info.total, 1 * reward);
assert_eq!(wallet1_info.amount_currently_spendable, 1 * reward);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 1);
Ok(())
})?;
// wallet1::account2 should have 2 in account1 (2 spendable)
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account2")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// check last confirmed height on this account is different from above (should be 0)
let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?;
assert_eq!(wallet1_info.last_confirmed_height, 3);
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 10);
assert_eq!(wallet1_info.total, 2 * reward);
assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward);
// check tx log as well
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 2);
Ok(())
})?;
// Make a transaction by sending 5 coins from wallet1::account1 -> wallet2::account2
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
}
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
// Send wallet inititates a standard transaction with --send=5
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
// Receiver gets their coins on account2 where they can payjoin
{
wallet_inst!(wallet2, w);
w.set_parent_key_id_by_name("account2")?;
}
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
// Receive wallet calls --receive=5
let args = &mut ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
};
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard2);
// Switch account for wallet1 to account2 and finish the transaction (should use account1 to complete)
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account2")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard3);
// post tx and mine a block to wallet1::account2
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
// The currently set account (account2) should not be affected by this transaction because they weren't a part of it,
// but it did mine a block and pick the transaction fees
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 11);
assert_eq!(
wallet1_info.total,
3 * reward + core::libtx::tx_fee(2, 2, 1) // we have received a block reward and the tx fee (payjoin)
);
assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 3);
Ok(())
})?;
// Switch to wallet1::account1 and check that it sent 5 coins
{
wallet_inst!(wallet1, w);
w.set_parent_key_id_by_name("account1")?;
}
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, 11);
assert_eq!(
wallet1_info.total,
1 * reward - 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution
);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 2);
Ok(())
})?;
// Switch to wallet2::account2 and check that it received 5 coins
{
wallet_inst!(wallet2, w);
w.set_parent_key_id_by_name("account2")?;
}
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet2_refreshed);
assert_eq!(wallet2_info.last_confirmed_height, 11);
assert_eq!(
wallet2_info.total,
4 * reward + 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution for a payjoin
);
let (_, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(txs.len(), 5);
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_accounts_switch() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_accounts_switch";
setup(test_dir);
contract_accounts_switch_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,103 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract early lock when using --add-outputs
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use grin_core::consensus;
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{OutputStatus, Slate, SlateState};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract new with --add-outputs
fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create a single wallet and mine 5 blocks
let (wallets, _chain, stopper, _bh) =
create_wallets(vec![vec![("default", 5)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
// Confirm all our outputs are unspent
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, commits) = api.retrieve_outputs(m, true, false, None)?;
for commit in commits.iter() {
assert_eq!(commit.output.status, OutputStatus::Unspent);
}
Ok(())
})?;
let mut slate = Slate::blank(0, false); // this gets overriden below
// Call contract 'new' with --add-outputs
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=80
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-80_000_000_000),
num_participants: 2,
add_outputs: true,
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
// Assert we locked 2 inputs and prepared an unconfirmed change output
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, commits) = api.retrieve_outputs(m, true, false, None)?;
// we locked the first two coinbase outputs
assert_eq!(commits[0].output.status, OutputStatus::Locked);
assert_eq!(commits[1].output.status, OutputStatus::Locked);
// we added a new unconfirmed change output
let new_output_idx = commits.len() - 1;
assert_eq!(
commits[new_output_idx].output.status,
OutputStatus::Unconfirmed
);
assert_eq!(
commits[new_output_idx].output.value,
2 * consensus::REWARD - 80_000_000_000 - my_fee_contribution(2, 1, 1, 2)?.fee()
);
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_early_lock_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_early_lock_tx";
setup(test_dir);
contract_early_lock_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,202 @@
// Copyright 2023 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Development and testing of early payment proofs, restricted at the moment
//! to contract-style transactions for experimental purposes
//!
//! https://github.com/mimblewimble/grin-rfcs/pull/70
//!
//!
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, Slatepack, Slatepacker, SlatepackerArgs, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// Development + Tests of early payment proof functionality - RSR workflow
fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin)
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
let mut sender_address = None;
// Get sender address explicitly
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
sender_address = Some(api.get_slatepack_address(send_mask, 0)?.pub_key);
Ok(())
})?;
let mut recipient_address = None;
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
// Receive wallet (invoice) calls --receive=5
let args = &mut ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
},
..Default::default()
};
args.setup_args.proof_args.sender_address = sender_address;
println!("SENDER ADDRESS: {:?}", sender_address);
slate = api.contract_new(m, args)?;
recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key);
Ok(())
})?;
assert_eq!(slate.state, SlateState::Invoice1);
println!("I1 State slate: {}", slate);
// Serialize slate into slatepack
let slatepacker_args = SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
};
let slate_packer = Slatepacker::new(slatepacker_args);
let slate_packed = slate_packer.create_slatepack(&slate).unwrap();
let slate_unpacked = slate_packer.get_slate(&slate_packed).unwrap();
println!("I2 Slate unpacked: {}", slate_unpacked);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Sending wallet (invoice) signs
let args = &ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
};
slate = api.contract_sign(m, &slate_unpacked, args)?;
Ok(())
})?;
println!("I2 State slate: {}", slate);
assert_eq!(slate.state, SlateState::Invoice2);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Invoice3);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in receive wallet
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len(), 5); // 4 mined and 1 received
let tx_log = txs[4].clone();
assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived);
assert_eq!(tx_log.amount_credited, 5_000_000_000);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
Ok(())
})?;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 5_000_000_000);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?));
Ok(())
})?;
let mut invoice_proof = None;
// Now some time has passed, sender retrieves and verify the payment proof
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, _m| {
// Extract the stored data as an invoice proof
invoice_proof =
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?);
Ok(())
})?;
let invoice_proof = invoice_proof.unwrap();
let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap();
// Should have all proof fields filled out
println!("INVOICE PROOF: {}", invoice_proof_json);
wallet::controller::foreign_single_use(recv_wallet.clone(), recv_mask.cloned(), |api| {
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap();
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?;
// tweak something and it shouldn't verify
proof.amount = 400000;
let retval = api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof);
assert!(retval.is_err());
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn contract_early_proofs_rsr() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_early_proofs_rsr";
setup(test_dir);
contract_early_proofs_rsr_test_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,182 @@
// Copyright 2023 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Development and testing of early payment proofs, restricted at the moment
//! to contract-style transactions for experimental purposes
//!
//! https://github.com/mimblewimble/grin-rfcs/pull/70
//!
//!
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// Development + Tests of early payment proof functionality
fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin)
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
let mut sender_address = None;
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=5
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
sender_address = Some(api.get_slatepack_address(send_mask, 0)?.pub_key);
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
let mut recipient_address = None;
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
// Receive wallet calls --receive=5
let args = &mut ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
};
// Note sender address explicity added here
args.proof_args.sender_address = sender_address;
slate = api.contract_sign(m, &slate, args)?;
recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key);
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard2);
// Send wallet finalizes and posts
//let mut sender_part_sig = None;
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard3);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in receive wallet
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len(), 5); // 4 mined and 1 received
let tx_log = txs[4].clone();
assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived);
assert_eq!(tx_log.amount_credited, 5_000_000_000);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
Ok(())
})?;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 5_000_000_000);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?));
Ok(())
})?;
let mut invoice_proof = None;
// Now some time has passed, sender retrieves and verify the payment proof
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, _m| {
// Extract the stored data as an invoice proof
invoice_proof =
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?);
Ok(())
})?;
let invoice_proof = invoice_proof.unwrap();
let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap();
// Should have all proof fields filled out
println!("INVOICE PROOF: {}", invoice_proof_json);
wallet::controller::foreign_single_use(recv_wallet.clone(), recv_mask.cloned(), |api| {
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap();
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?;
// tweak something and it shouldn't verify
proof.amount = 400000;
let retval = api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof);
assert!(retval.is_err());
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn contract_early_proofs_srs() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_early_proofs_src";
setup(test_dir);
contract_early_proofs_srs_test_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,153 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract RSR flow
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract RSR flow
fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin)
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
// Receive wallet inititates an invoice transaction with --receive=5
let args = &mut ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
},
..Default::default()
};
args.setup_args.proof_args.suppress_proof = true;
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Invoice1);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send Wallet calls --send=5
let args = &ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Invoice2);
// Receive wallet finalizes and posts
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let args = &mut ContractSetupArgsAPI {
..Default::default()
};
// TODO: This possibly shouldn't be needed here if no proof?
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Invoice3);
// Send wallet posts so receive wallet doesn't get the mined amount
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in receive wallet
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len(), 5); // 4 mined and 1 received
let tx_log = txs[4].clone();
assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived);
assert_eq!(tx_log.amount_credited, 5_000_000_000);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
// println!("txlogentry: {:#?}", tx_log);
// println!("wallet info: {:#?}", wallet_info);
// let (validated, commits) = api.retrieve_outputs(m, true, false, Some(tx_log.id))?;
// println!("commits: {:#?}", commits);
// panic!("lala");
Ok(())
})?;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh - 4 + 1); // send_wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 5_000_000_000);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?));
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_rsr_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_rsr_tx";
setup(test_dir);
contract_rsr_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,111 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract self-spend flow
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract self-spend flow
fn contract_self_spend_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create a single wallet and mine 4 blocks
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=0
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(0),
num_participants: 1,
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
// In the case of a self-spend, we just finish the slate when it's in the Standard2 state
assert_eq!(slate.state, SlateState::Standard2);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpend);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 1)?));
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_self_spend_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_self_spend_tx";
setup(test_dir);
contract_self_spend_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,93 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract self-spend flow
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract self-spend flow
fn contract_self_spend_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create a single wallet and mine 4 blocks
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet initiates a standard transaction with --send=0
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(0),
num_participants: 1,
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.cancel_tx(m, None, Some(slate.id))?;
Ok(())
})?;
// Assert tx log has been cancelled
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let query_args = libwallet::RetrieveTxQueryArgs {
exclude_cancelled: Some(false),
..Default::default()
};
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, Some(query_args))?;
assert!(refreshed);
assert_eq!(txs.len() as u64, 5); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[4].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpendCancelled);
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_self_spend_cancel() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_self_spend_cancel";
setup(test_dir);
contract_self_spend_cancel_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,160 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract self-spend flow by using custom inputs and creating custom outputs
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use grin_core::consensus;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs};
use libwallet::{OutputStatus, Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract self-spend flow with custom picked inputs and outputs
fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create a single wallet and mine 4 blocks
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 10)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let mut use_inputs = String::from("");
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, commits) = api.retrieve_outputs(m, true, false, None)?;
println!("OOOT: {:?}", commits[0].output);
use_inputs = format!(
"{},{}",
commits[0].output.commit.as_ref().unwrap(),
commits[1].output.commit.as_ref().unwrap()
);
Ok(())
})?;
let mut slate = Slate::blank(0, true); // this gets overriden below
let selection_args = OutputSelectionArgs {
min_input_confirmation: 0,
use_inputs: Some(use_inputs.clone()), // we will use two coinbase inputs
make_outputs: Some(String::from("88,35,3,0.2,15")), // the sum is such that it will need to pick another input making total of 3 inputs
};
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=0
let args = &ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(0),
num_participants: 1,
selection_args: selection_args.clone(),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let args = &ContractSetupArgsAPI {
..Default::default()
};
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
// In the case of a self-spend, we just finish the slate when it's in the Standard2 state
assert_eq!(slate.state, SlateState::Standard2);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpend);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 3);
assert_eq!(tx_log.num_outputs, 6);
assert_eq!(tx_log.fee, Some(my_fee_contribution(3, 6, 1, 1)?));
Ok(())
})?;
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, commits) = api.retrieve_outputs(m, true, false, None)?;
// Assert used inputs are the ones we specified
let used_inputs = use_inputs.split(",").collect::<Vec<&str>>();
assert_eq!(commits[0].output.status, OutputStatus::Spent);
assert_eq!(commits[0].output.commit.as_ref().unwrap(), used_inputs[0]);
assert_eq!(commits[1].output.status, OutputStatus::Spent);
assert_eq!(commits[1].output.commit.as_ref().unwrap(), used_inputs[1]);
assert_eq!(commits[2].output.status, OutputStatus::Spent);
// Assert expected outputs were created
// 88, 35, 3, 0.2, 15 and a change output
assert_eq!(commits[10].output.value, 88 * consensus::GRIN_BASE);
assert_eq!(commits[11].output.value, 35 * consensus::GRIN_BASE);
assert_eq!(commits[12].output.value, 3 * consensus::GRIN_BASE);
assert_eq!(
commits[13].output.value,
(0.2 * consensus::GRIN_BASE as f64) as u64
);
assert_eq!(commits[14].output.value, 15 * consensus::GRIN_BASE);
// change output is 3*reward - (88-35-3-0.2-15) - my_fees
assert_eq!(
commits[15].output.value,
3 * consensus::REWARD
- selection_args.sum_output_amounts()
- my_fee_contribution(3, 6, 1, 1)?.fee()
);
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_self_spend_custom_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_self_spend_custom_tx";
setup(test_dir);
contract_self_spend_custom_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,146 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract SRS flow
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract SRS flow
fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin)
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=5
let args = &mut ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
// Receive wallet calls --receive=5
let args = &mut ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
};
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard2);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let args = &mut ContractSetupArgsAPI {
..Default::default()
};
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard3);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
api.post_tx(m, &slate, false)?;
Ok(())
})?;
bh += 1;
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in receive wallet
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len(), 5); // 4 mined and 1 received
let tx_log = txs[4].clone();
assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived);
assert_eq!(tx_log.amount_credited, 5_000_000_000);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
Ok(())
})?;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 5_000_000_000);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?));
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_srs_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_srs_tx";
setup(test_dir);
contract_srs_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,156 @@
// Copyright 2022 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet doing contract SRS flow
// #[macro_use]
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
extern crate log;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self};
use libwallet::contract::my_fee_contribution;
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use libwallet::mwmixnet::onion::crypto::secp;
use libwallet::mwmixnet::types::MixnetReqCreationParams;
use libwallet::{Slate, SlateState, TxLogEntryType};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallets, setup};
/// contract SRS flow - just creating an mwmixnet tx at the moment
fn contract_srs_mwmixnet_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin)
let (wallets, chain, stopper, mut bh) =
create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap();
let send_wallet = wallets[0].0.clone();
let send_mask = wallets[0].1.as_ref();
let recv_wallet = wallets[1].0.clone();
let recv_mask = wallets[1].1.as_ref();
let mut slate = Slate::blank(0, true); // this gets overriden below
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
// Send wallet inititates a standard transaction with --send=5
let args = &mut ContractNewArgsAPI {
setup_args: ContractSetupArgsAPI {
net_change: Some(-5_000_000_000),
..Default::default()
},
..Default::default()
};
slate = api.contract_new(m, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard1);
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
// Receive wallet calls --receive=5
let args = &mut ContractSetupArgsAPI {
net_change: Some(5_000_000_000),
..Default::default()
};
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard2);
// Send wallet finalizes and posts
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let args = &mut ContractSetupArgsAPI {
..Default::default()
};
args.proof_args.suppress_proof = true;
slate = api.contract_sign(m, &slate, args)?;
Ok(())
})?;
assert_eq!(slate.state, SlateState::Standard3);
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let server_key_1 = secp::random_secret();
let server_key_2 = secp::random_secret();
let params = MixnetReqCreationParams {
server_keys: vec![server_key_1, server_key_2],
fee_per_hop: 50_000_000,
};
//api.create_mwmixnet_req(send_mask, &params, &slate)?;
Ok(())
})?;
bh += 1;
/*
let _ =
test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false);
bh += 3;
// Assert changes in receive wallet
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len(), 5); // 4 mined and 1 received
let tx_log = txs[4].clone();
assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived);
assert_eq!(tx_log.amount_credited, 5_000_000_000);
assert_eq!(tx_log.amount_debited, 0);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?);
assert_eq!(tx_log.fee, expected_fees_paid);
assert_eq!(
wallet_info.amount_currently_spendable,
4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid
);
Ok(())
})?;
// Assert changes in send wallet
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?;
let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?;
assert_eq!(wallet_info.last_confirmed_height, bh);
assert!(refreshed);
assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx
let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4?
assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent);
assert_eq!(tx_log.amount_credited, 0);
assert_eq!(tx_log.amount_debited, 5_000_000_000);
assert_eq!(tx_log.num_inputs, 1);
assert_eq!(tx_log.num_outputs, 1);
assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?));
Ok(())
})?;*/
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(200));
Ok(())
}
#[test]
fn wallet_contract_srs_mwmixnet_tx() -> Result<(), libwallet::Error> {
let test_dir = "test_output/contract_srs_mwmixnet_tx";
setup(test_dir);
contract_srs_mwmixnet_tx_impl(test_dir)?;
clean_output_dir(test_dir);
Ok(())
}

View file

@ -0,0 +1,125 @@
// Copyright 2021 The Grin Developers
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Test a wallet sending to self
#[macro_use]
extern crate log;
extern crate grin_wallet_controller as wallet;
extern crate grin_wallet_impls as impls;
use grin_core as core;
use grin_wallet_libwallet as libwallet;
use impls::test_framework::{self, LocalWalletClient};
use libwallet::{InitTxArgs, RetrieveTxQueryArgs};
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
#[macro_use]
mod common;
use common::{clean_output_dir, create_wallet_proxy, setup};
/// self send impl
fn self_send_cancel_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
// 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();
let stopper = wallet_proxy.running.clone();
// Create a new wallet test client, and set its queues to communicate with the
// proxy
create_wallet_and_add!(
client1,
wallet1,
mask1_i,
test_dir,
"wallet1",
None,
&mut wallet_proxy,
true
);
let mask1 = (&mask1_i).as_ref();
// Set the wallet proxy listener running
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
}
});
// few values to keep things shorter
let reward = core::consensus::REWARD;
let mut bh = 10u64;
let _ =
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
assert!(wallet1_refreshed);
assert_eq!(wallet1_info.last_confirmed_height, bh);
assert_eq!(wallet1_info.total, bh * reward);
// send to send
let args = InitTxArgs {
src_acct_name: None,
amount: reward * 2,
minimum_confirmations: 2,
max_outputs: 500,
num_change_outputs: 1,
selection_strategy_is_use_all: true,
..Default::default()
};
let mut slate = api.init_send_tx(m, args)?;
api.tx_lock_outputs(m, &slate)?;
// Send directly to self
wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| {
slate = api.receive_tx(&slate, None, None)?;
Ok(())
})?;
// Now cancel the transaction
api.cancel_tx(mask1, None, Some(slate.id.clone()))?;
bh += 1;
Ok(())
})?;
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
let query = RetrieveTxQueryArgs {
include_outstanding_only: Some(true),
exclude_cancelled: Some(true),
..Default::default()
};
let txs = api.retrieve_txs(mask1, true, None, None, Some(query))?;
for tx in txs.1.iter() {
println!("Tx: {:?}", tx);
}
assert!(txs.1.is_empty());
Ok(())
})?;
// let logging finish
stopper.store(false, Ordering::Relaxed);
thread::sleep(Duration::from_millis(1000));
Ok(())
}
#[test]
fn wallet_self_send_cancel() {
let test_dir = "test_output/self_send_cancel";
setup(test_dir);
if let Err(e) = self_send_cancel_test_impl(test_dir) {
panic!("Libwallet Error: {}", e);
}
clean_output_dir(test_dir);
}

18
default.nix Normal file
View file

@ -0,0 +1,18 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [ pkgs.clang ];
buildInputs = with pkgs; [
glibc
rustup
openssl
pkgconfig
llvmPackages.libclang
ncurses
glibcLocales
tor
];
shellHook = ''
export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
'';
}

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_impls"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Concrete types derived from libwallet traits"
license = "Apache-2.0"
@ -36,19 +36,19 @@ sysinfo = "0.29"
base64 = "0.12.0"
url = "2.1"
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "../util", version = "5.4.0-contracts.0" }
grin_wallet_config = { path = "../config", version = "5.4.0-contracts.0" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-contracts.0" }
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_keychain = "5.4.0-alpha.1"
# grin_chain = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_api = "5.4.0-alpha.1"
# grin_store = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_keychain = "5.4.0-contracts.0"
# grin_chain = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# grin_api = "5.4.0-contracts.0"
# grin_store = "5.4.0-contracts.0"
# For beta release

View file

@ -179,6 +179,10 @@ impl HttpSlateSender {
return Err(Error::ClientCallback(report));
}
if supported_slate_versions.contains(&"V5".to_owned()) {
return Ok(SlateVersion::V5);
}
if supported_slate_versions.contains(&"V4".to_owned()) {
return Ok(SlateVersion::V4);
}
@ -223,6 +227,7 @@ impl SlateSender for HttpSlateSender {
self.launch_tor()?;
let slate_send = match self.check_other_version(&url_str)? {
SlateVersion::V5 => VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?,
SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?,
};
// Note: not using easy-jsonrpc as don't want the dependencies in this crate

View file

@ -17,7 +17,7 @@ pub mod http;
mod slatepack;
pub use self::file::PathToSlate;
pub use self::http::{HttpSlateSender, SchemeNotHttp};
pub use self::http::HttpSlateSender;
pub use self::slatepack::PathToSlatepack;
use crate::config::WalletConfig;

View file

@ -21,8 +21,6 @@ use std::io::{Read, Write};
use std::marker::PhantomData;
use std::path::Path;
use uuid::Uuid;
use crate::blake2::blake2b::{Blake2b, Blake2bResult};
use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain, SwitchCommitmentType};
@ -318,9 +316,41 @@ where
Box::new(iter)
}
fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec());
self.db.get_ser(&key, None).map_err(|e| e.into())
// TODO: I think this can be deleted
// fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
// let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec());
// self.db.get_ser(&key, None).map_err(|e| e.into())
// }
fn get_tx_log_entry(
&self,
parent_id: Identifier,
log_id: u32,
) -> Result<Option<TxLogEntry>, Error> {
let tx_log_key = to_key_u64(
TX_LOG_ENTRY_PREFIX,
&mut parent_id.to_bytes().to_vec(),
log_id as u64,
);
self.db.get_ser(&tx_log_key, None).map_err(|e| e.into())
/*
fn save_tx_log_entry(
&mut self,
tx_in: TxLogEntry,
parent_id: &Identifier,
) -> Result<(), Error> {
let tx_log_key = to_key_u64(
TX_LOG_ENTRY_PREFIX,
&mut parent_id.to_bytes().to_vec(),
tx_in.id as u64,
);
self.db
.borrow()
.as_ref()
.unwrap()
.put_ser(&tx_log_key, &tx_in)?;
*/
}
// TODO - fix this awkward conversion between PrefixIterator and our Box<dyn Iterator>

View file

@ -14,7 +14,7 @@
// Derived from https://github.com/apoelstra/rust-jsonrpc
//! JSON RPC Types for V2 node client
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct GetTipResp {
pub height: u64,

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_libwallet"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
license = "Apache-2.0"
@ -16,6 +16,7 @@ rand = "0.6"
serde = "1"
serde_derive = "1"
serde_json = "1"
serde_with = { version = "1", features = ["chrono"] }
log = "0.4"
uuid = { version = "0.8", features = ["serde", "v4"] }
chrono = { version = "0.4.11", features = ["serde"] }
@ -23,11 +24,11 @@ lazy_static = "1"
strum = "0.18"
strum_macros = "0.18"
thiserror = "1"
ed25519-dalek = "1.0.0-pre.4"
ed25519-dalek = "1.0.1"
x25519-dalek = "0.6"
base64 = "0.9"
regex = "1.3"
sha2 = "0.8"
sha2 = "0.10.0"
bs58 = "0.3"
age = "0.7"
curve25519-dalek = "2.1"
@ -36,16 +37,22 @@ bech32 = "0.7"
byteorder = "1.3"
num-bigint = "0.2"
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
grin_wallet_util = { path = "../util", version = "5.4.0-contracts.0" }
grin_wallet_config = { path = "../config", version = "5.4.0-contracts.0" }
grin_secp256k1zkp = { version = "0.7.12", features = ["bullet-proof-sizing"]}
#mwmixnet onion
chacha20 = "0.8.1"
hmac = { version = "0.12.0", features = ["std"]}
##### Grin Imports
# For Release
# grin_core = "5.4.0-alpha.1"
# grin_keychain = "5.4.0-alpha.1"
# grin_util = "5.4.0-alpha.1"
# grin_store = "5.4.0-alpha.1"
# grin_core = "5.4.0-contracts.0"
# grin_keychain = "5.4.0-contracts.0"
# grin_util = "5.4.0-contracts.0"
# grin_store = "5.4.0-contracts.0"
# For beta release
@ -66,4 +73,8 @@ grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" }
# grin_util = { path = "../../grin/util"}
# grin_store = { path = "../../grin/store"}
# mw-mixnet
#####

View file

@ -15,7 +15,11 @@
//! Generic implementation of owner API functions
use strum::IntoEnumIterator;
use crate::api_impl::owner::contract_new as owner_contract_new;
use crate::api_impl::owner::contract_sign as owner_contract_sign;
use crate::api_impl::owner::{check_ttl, post_tx};
use crate::contract::proofs::InvoiceProof;
use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
use crate::grin_core::core::FeeFields;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
@ -25,6 +29,7 @@ use crate::{
address, BlockFees, CbData, Error, NodeClient, Slate, SlateState, TxLogEntryType, VersionInfo,
WalletBackend,
};
use ed25519_dalek::PublicKey as DalekPublicKey;
use super::owner::tx_lock_outputs;
@ -114,14 +119,16 @@ where
let excess = ret_slate.calc_excess(keychain.secp())?;
if let Some(ref mut p) = ret_slate.payment_proof {
if let Some(saddr) = p.sender_address {
let sig = tx::create_payment_proof_signature(
ret_slate.amount,
&excess,
p.sender_address,
saddr,
address::address_from_derivation_path(&keychain, &parent_key_id, 0)?,
)?;
p.receiver_signature = Some(sig);
p.promise_signature = Some(sig);
}
}
ret_slate.amount = 0;
@ -231,3 +238,94 @@ where
}
Ok(sl)
}
/// Initialize a receive transaction contract
pub fn contract_new<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractNewArgsAPI,
// use_test_rng: bool,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let net_change = args.setup_args.net_change.unwrap();
if net_change <= 0 {
return Err(Error::GenericError(
"Can't create a non-receiving contract from a foreign API.".to_string(),
)
.into());
}
owner_contract_new(&mut *w, keychain_mask, args)
}
/// Sign a receive transaction contract
pub fn contract_sign<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractSetupArgsAPI,
slate: &Slate,
// use_test_rng: bool,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let net_change = args.net_change.unwrap();
if net_change <= 0 {
return Err(Error::GenericError(
"Can't sign a non-receiving contract from a foreign API.".to_string(),
)
.into());
}
owner_contract_sign(&mut *w, keychain_mask, args, slate)
}
/// Verify an invoice payment proof
pub fn verify_payment_proof_invoice<'a, T: ?Sized, C, K>(
w: &mut T,
recipient_address: &DalekPublicKey,
proof: &InvoiceProof,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut client = w.w2n_client().clone();
let wd = match proof.witness_data.clone() {
Some(w) => w,
None => {
return Err(Error::PaymentProof(format!(
"Cannot verify invoice proof with no witness data",
)))
}
};
let (retrieved_kernel, _) = match client.get_kernel(&wd.kernel_commitment, None, None) {
Err(e) => {
return Err(Error::PaymentProof(format!(
"Error retrieving kernel from chain: {}",
e
)));
}
Ok(None) => {
return Err(Error::PaymentProof(format!(
"Transaction kernel with excess {:?} not found on chain",
wd.kernel_commitment
)));
}
Ok(Some((k, _, index))) => (k, index),
};
// Now verify with retrieved data
proof.verify_witness(
recipient_address,
&retrieved_kernel.excess_sig,
&retrieved_kernel.msg_to_sign()?,
)
}

View file

@ -17,27 +17,35 @@
use uuid::Uuid;
use crate::api_impl::foreign::finalize_tx as foreign_finalize;
use crate::contract::proofs::{InvoiceProof, ProofWitness};
use crate::grin_core::core::hash::Hashed;
use crate::grin_core::core::{Output, OutputFeatures, Transaction};
use crate::grin_core::libtx::proof;
use crate::grin_keychain::ViewKey;
use crate::grin_util::secp::key::SecretKey;
use crate::grin_util::Mutex;
use crate::grin_util::ToHex;
use crate::grin_util::secp::pedersen;
use crate::grin_util::{static_secp_instance, Mutex, ToHex};
use crate::util::{OnionV3Address, OnionV3AddressError};
use crate::api_impl::owner_updater::StatusMessage;
use crate::contract::types::{ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI};
use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType};
use crate::mwmixnet::onion::create_onion;
use crate::mwmixnet::types::{
add_excess, new_hop, random_secret, ComSignature, Hop, MixnetReqCreationParams, SwapReq,
};
use crate::internal::{keys, scan, selection, tx, updater};
use crate::slate::{PaymentInfo, Slate, SlateState};
use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo};
use crate::Error;
use crate::{
address, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult,
address, contract, wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult,
OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack,
SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus,
WalletInst, WalletLCProvider,
};
use chrono::prelude::{DateTime, NaiveDateTime, Utc};
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::SecretKey as DalekSecretKey;
use ed25519_dalek::Verifier;
@ -473,6 +481,145 @@ where
})
}
/// Retrieve invoice payment proof
/// TODO: Need to unify with legacy above
pub fn retrieve_payment_proof_invoice<'a, L, C, K>(
wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
keychain_mask: Option<&SecretKey>,
status_send_channel: &Option<Sender<StatusMessage>>,
refresh_from_node: bool,
tx_id: Option<u32>,
tx_slate_id: Option<Uuid>,
) -> Result<InvoiceProof, Error>
where
L: WalletLCProvider<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
if tx_id.is_none() && tx_slate_id.is_none() {
return Err(Error::PaymentProofRetrieval(
"Transaction ID or Slate UUID must be specified".to_owned(),
));
}
if refresh_from_node {
update_wallet_state(
wallet_inst.clone(),
keychain_mask,
status_send_channel,
false,
)?
} else {
false
};
let txs = retrieve_txs(
wallet_inst.clone(),
keychain_mask,
status_send_channel,
refresh_from_node,
tx_id,
tx_slate_id,
None,
)?;
if txs.1.len() != 1 {
return Err(Error::PaymentProofRetrieval(
"Transaction doesn't exist".to_owned(),
));
}
// Pull out all needed fields, returning an error if they're not present
let tx = txs.1[0].clone();
let amount = if tx.amount_credited >= tx.amount_debited {
tx.amount_credited - tx.amount_debited
} else {
// TODO: Invoice proof not expecting fee included here
tx.amount_debited - tx.amount_credited
};
let (mut proof, sender_part_sig) = match tx.payment_proof {
Some(p) => {
if p.receiver_public_nonce.is_none() {
return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored receiver public nonce".into(),
));
};
if p.receiver_public_excess.is_none() {
return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored receiver public excess".into(),
));
};
if p.timestamp.is_none() {
return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored timestamp".into(),
));
};
if p.sender_part_sig.is_none() {
return Err(Error::PaymentProofRetrieval(
"Invoice Proof requires stored sender partial signature".into(),
));
};
(
InvoiceProof {
proof_type: if let Some(t) = p.proof_type { t } else { 1u8 },
amount,
receiver_public_nonce: p.receiver_public_nonce.unwrap(),
receiver_public_excess: p.receiver_public_excess.unwrap(),
sender_address: p.sender_address,
timestamp: p.timestamp.unwrap().timestamp(),
memo: p.memo,
promise_signature: p.promise_signature,
witness_data: None,
},
p.sender_part_sig.unwrap(),
)
}
None => {
return Err(Error::PaymentProofRetrieval(
"Transaction does not contain a payment proof".to_owned(),
));
}
};
// Now to kernel lookup, to fill in the witness data
// Check kernel exists
let mut client = {
wallet_lock!(wallet_inst, w);
w.w2n_client().clone()
};
let kernel_excess = match tx.kernel_excess {
Some(k) => k,
None => {
return Err(Error::PaymentProofRetrieval(format!(
"Invoice proof transaction kernel excess missing",
)))
}
};
let (retrieved_kernel, index) = match client.get_kernel(&kernel_excess, None, None) {
Err(e) => {
return Err(Error::PaymentProof(format!(
"Error retrieving kernel from chain: {}",
e
)));
}
Ok(None) => {
return Err(Error::PaymentProof(format!(
"Transaction kernel with excess {:?} not found on chain",
kernel_excess
)));
}
Ok(Some((k, _, index))) => (k, index),
};
proof.witness_data = Some(ProofWitness {
kernel_index: index,
kernel_commitment: retrieved_kernel.excess,
sender_partial_sig: sender_part_sig,
});
Ok(proof)
}
/// Initiate tx as sender
pub fn init_send_tx<'a, T: ?Sized, C, K>(
w: &mut T,
@ -568,9 +715,14 @@ where
let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?;
slate.payment_proof = Some(PaymentInfo {
sender_address: sender_address.to_ed25519()?,
sender_address: Some(sender_address.to_ed25519()?),
receiver_address: a.pub_key,
receiver_signature: None,
promise_signature: None,
timestamp: DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
Utc,
),
memo: None,
});
context.payment_proof_derivation_index = Some(deriv_path);
@ -1369,3 +1521,143 @@ where
output: output,
})
}
// Contract implementation
/// Initialize transaction contract
pub fn contract_new<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractNewArgsAPI,
// use_test_rng: bool,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
contract::new(&mut *w, keychain_mask, &args.setup_args)
}
// /// Setup transaction contract
// pub fn contract_setup<'a, T: ?Sized, C, K>(
// w: &mut T,
// keychain_mask: Option<&SecretKey>,
// args: &ContractSetupArgsAPI,
// slate: &Slate,
// // use_test_rng: bool,
// ) -> Result<Slate, Error>
// where
// T: WalletBackend<'a, C, K>,
// C: NodeClient + 'a,
// K: Keychain + 'a,
// {
// contract::setup(&mut *w, keychain_mask, slate, &args)
// }
/// Sign transaction contract
pub fn contract_sign<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractSetupArgsAPI,
slate: &Slate,
// use_test_rng: bool,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
contract::sign(&mut *w, keychain_mask, slate, &args)
}
/// Revoke transaction contract
pub fn contract_revoke<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractRevokeArgsAPI,
// use_test_rng: bool,
) -> Result<Option<Slate>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
contract::revoke(&mut *w, keychain_mask, &args)
}
/// Revoke transaction contract
pub fn get_slate_index_matching_my_context<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
// use_test_rng: bool,
) -> Result<usize, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let keychain = w.keychain(keychain_mask)?;
let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?;
slate.find_index_matching_context(&keychain, &context)
}
/// Create MXMixnet request
pub fn create_mwmixnet_req<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
params: &MixnetReqCreationParams,
slate: &Slate,
// use_test_rng: bool,
) -> Result<SwapReq, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?;
let my_keys = context.get_private_keys();
let kernel = slate.tx_or_err()?.kernels()[0];
let msg = kernel.msg_to_sign()?;
let comsig = ComSignature::sign(slate.amount, &my_keys.0, &msg.to_hex().as_bytes().to_vec())?;
let mut hops: Vec<Hop> = Vec::new();
let mut final_commit = kernel.excess.clone();
let mut final_blind = my_keys.0.clone();
for i in 0..params.server_keys.len() {
let excess = params.server_keys[i].clone();
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
final_blind.add_assign(&secp, &excess).unwrap();
final_commit = add_excess(&final_commit, &excess).unwrap();
let proof = if i == params.server_keys.len() - 1 {
let n1 = random_secret();
let rp = secp.bullet_proof(
slate.amount - (params.fee_per_hop * params.server_keys.len() as u32) as u64,
final_blind.clone(),
n1.clone(),
n1.clone(),
None,
None,
);
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
Some(rp)
} else {
None
};
let hop = new_hop(&params.server_keys[i], &excess, params.fee_per_hop, proof);
hops.push(hop);
}
let onion = create_onion(&kernel.excess, &hops)?;
Ok(SwapReq { comsig, onion })
//slate.find_index_matching_context(&keychain, &context)
}

View file

@ -200,6 +200,8 @@ pub struct RetrieveTxQueryArgs {
pub include_received_only: Option<bool>,
/// whether to only consider coinbase transactions
pub include_coinbase_only: Option<bool>,
/// whether to only consider self spend transactions
pub include_self_spend_only: Option<bool>,
/// whether to only consider reverted transactions
pub include_reverted_only: Option<bool>,
/// lower bound on the total amount (amount_credited - amount_debited), inclusive
@ -237,6 +239,7 @@ impl Default for RetrieveTxQueryArgs {
include_sent_only: Some(false),
include_received_only: Some(false),
include_coinbase_only: Some(false),
include_self_spend_only: Some(false),
include_reverted_only: Some(false),
min_amount: None,
max_amount: None,

View file

@ -0,0 +1,116 @@
# Contract actions
### API endpoints
We introduce 3 new API endpoints `/new, /setup, /sign` each corresponding to a specific action on a contract. In the future we'll add the ability to also `view` a contract and to `revoke` it.
### Rust implementation
Every contract action on a slate is divided in 3 parts:
1. compute the new state
2. save the new state
3. return slate
Putting this into code, it looks like the following:
```rust
// Compute the new state (both of the Slate and the Context)
let (slate, context) = compute(slate, args);
// Atomically commit the new state
contract_utils::save_step(slate, context, ...);
// Return the newly produced slate
return slate;
```
We only allow contribution of custom inputs/outputs when we're doing the setup phase. Once the setup phase is done,
we no longer allow any customization of inputs. This means that the customization can only happen at contract setup phase which is the first time we see the contract.Additionally, if we customize output selection, we immediately pick the inputs/outputs which means it's an early lock. These are however not added to the slate until we reach the 'sign' phase of the contract. Counterparties don't need to see our inputs/outputs before that. This means we always add inputs/outputs only when we have to and never before.
Ideally we'd also separate side effects out of these functions e.g. computing the current_height
or refreshing the outputs with updater::refresh_outputs(...). The current_height could be
communicated through a &ChainState parameter which would collect these values before the call.
Additionally, we could fetch the existing Context before the call to avoid doing db fetch.
Separating side effects until the 'save_step' part would make these functions much easier to test.
#### TODOs
- for payjoins, instead of doing Some("any"), find an actual input and put the actual commitment in --use-inputs (only do that if none is added). Think about how to do that in a way that would be nice also for the API. Maybe the API should just take "any" which gets transformed into one of the random inputs. Maybe the "any" is ok. It seems to work fine, might be better if we selected the input though.
- make sure to forget the Context when we sign or at least forget the secret keys for it to avoid signing with the same nonce twice
- make_outputs api should receive nanogrins rather than grin. We have to make the conversion before calling the API
- sometimes the slatepack outputs with a \n for some reason which makes pasting it register \n as the end of paste and crashes?
- remove casting decimals, we should accept value in nanogrins (including --make-outputs option), never as 0.1 Grin through the interface. Casting should be left to the gui wallet logic
- Check casts to/from i64/u64 etc. consider using saturating methods. Make sure conversions are safe.
- separate side-effects out from the main computations
- is keys::next_available_key(..) safe from race-conditions? (do we lock?)
- function add_output_to_ctx has a comment around next_available_key
- add support for different accounts not just main (check parent_id, parent_key_id, etc. usage)
- ensure counterparty can't make you overpay fees through num_participants param
- make sure the stored transaction is saved correctly at each step (TxLogEntry has stored_tx field, check other fields as well)
- make sure the transaction log contains all the necessary data (check TODO comments on tx log entry functions)
- graceful error handling
- setup.rs# TODO: verify that the parent_key_id is consistent
- replace mutable objects with immutable when possible
- make sure we lock the wallet when needed (check wallet_lock!() macro that is used in api/owner.rs)
- add support for more than 2 parties (includes a new 'setup' API endpoint and command)
- do we avoid using "too recent" outputs? e.g. though with depth < 10
- remove unneeded imports
- think if Context.setup_args.net_change type should be u64. If you make it i64, you divide it's size by 2. Perhaps it would be
better to have a u64 field and another field called 'positive' of type bool.
- add --no-setup to 'new' command
- add early-payment proofs. Make sure we have a symmetric variant of a payment proof to avoid having different proofs based on which position you are in the contract signing. Ideally, the position would be irrelevant.
- what happens if you call contract sign on some slate that was not initiated as a contract slate and has different context values?
- make sure you handle all the flows with coinbase outputs as well
- check if they can trick you by providing a slate with different inputs/outputs that are yours
- remove 'setup' API/CLI
- move the contract test utilities to a separate contract_utilities file instead of having it in 'common/mod.rs'
- we have --add-outputs, but we should also lock if we use the --use-inputs param
- check if contract_accounts_switch.rs is a legit scenario. It might need to return an error if the wallet is trying to sign with a different account
#### Tests
- test contract_fee (various test around fee contribution with 1 or 2 parties)
- test save_step functionality (stored tx, context, logs,..)
- test different output selection in step1 and step3
- test foreign API for contract new and sign
- test a case where the receiver doesn't have an input available (either not enough confirmations or no inputs)
- contract_rsr.rs asserts that you get amount_credited=5, should it subtract the fees?
- test using more than a single input
- test that sending then again the same slatepack doesn't produce a new signature (to avoid leaking key)
- test locking:
* test that outputs are locked after you sign
* test early locking when using --make-outputs or --use-inputs etc.
- test --no-payjoin
- test accounts
- test 0-value outputs
- test that if --no-payjoin is used, this doesn't mean that we early lock (we shouldn't). Same for --make-outputs
- test slate content through steps
- test negative cases (not enough funds, using input that doesn't exist, make outputs that go over the value, sign twice,...)
#### DONE
- Always "late-add" inputs/outputs to the slate
- _Always_ add a change output, even if the change output ends up being a 0-value output
#### save_step
// TODO:
// - is_signed should be derived from the slate
// - Check what happens if the batch fails. Also think about possible race conditions because
// of the time delay between the id was picked and saved.
// - Consider taking ownership of Context here. It should not be used after this is called.
### Side-effects
#### Setup
// Side-effects:
// - height = w.w2n_client().get_chain_tip()?.0;
// - maybe_context = w.get_private_context(keychain_mask, sl.id.as_bytes())
// - create_contract_ctx -> updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?;
// - add_outputs -> let current_height = w.w2n_client().get_chain_tip()?.0;
// - add_outputs -> contribute_output -> let key_id = keys::next_available_key(wallet, keychain_mask).unwrap();
// - TODO: would we need to compute keys::next_available_key for as many outputs as we plan to contribute and pass
// them as a param to keep this without side effects?
#### Sign
// Side-effects:
// - contract_utils::check_already_signed -> tx_log_iter
// - contract_utils::get_net_change -> context and net_change
// - everything from 'setup'

View file

@ -0,0 +1,27 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module contains contract related actions.
mod new;
mod revoke;
mod setup;
mod sign;
mod view;
pub use self::new::new;
pub use self::revoke::revoke;
pub use self::setup::setup;
pub use self::sign::sign;
pub use self::view::view;

View file

@ -0,0 +1,76 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of contract new
use crate::contract;
use crate::contract::actions::setup;
use crate::contract::types::ContractSetupArgsAPI;
use crate::error::Error;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate;
use crate::types::{Context, NodeClient, WalletBackend};
/// Create a new contract with initial setup done by the initiator
pub fn new<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
setup_args: &ContractSetupArgsAPI,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Compute state for 'new'
let (slate, mut context) = compute(w, keychain_mask, setup_args)?;
// Atomically commit state
contract::utils::save_step(
w,
keychain_mask,
&slate,
&mut context,
setup_args.add_outputs,
false,
)?;
Ok(slate)
}
/// Compute logic for new
pub fn compute<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
setup_args: &ContractSetupArgsAPI,
) -> Result<(Slate, Context), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let net_change = setup_args.net_change.unwrap();
debug!("contract::new => net_change passed: {}", net_change);
// 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);
// We set slate.amount to contain the _positive_ net_change for the other party so they can derive expectations.
slate.amount = net_change.abs() as u64;
debug!("contract::new => slate amount: {}", slate.amount);
// Perform setup for the slate
setup::compute(w, keychain_mask, &mut slate, setup_args)
}

View file

@ -0,0 +1,103 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of contract revoke
use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs};
use crate::contract::{new, sign};
use crate::error::Error;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::internal::tx;
use crate::slate::Slate;
use crate::types::{NodeClient, OutputData, OutputStatus, WalletBackend};
/// Contract revocation is done by double-spending the input
pub fn revoke<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
args: &ContractRevokeArgsAPI,
) -> Result<Option<Slate>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// TODO: check the correctness of this. This is essentially old cancel + self-spend.
// FUTURE: we may want to boost fees in case we notice something in the mempool. There
// are also race conditions possible. We may not want to label txlogenry as Canceled
// until the new tx gets on the chain.
// NOTE: We should not care about deleting the context because as soon as we sign
// a contract, the context is deleted.
// If we contributed inputs, we must have locked them at which point we also set the
// OutputData.tx_log_entry which is the tx_id.
let tx_id = args.tx_id;
// Find my outputs that have been Locked and refer to the given tx_id
let my_contributed_inputs = w
.batch(keychain_mask)?
.iter()
.filter(|out| {
// Find an output that is Locked and is in the tx_input_commit
out.status == OutputStatus::Locked
&& (out.tx_log_entry.is_some() && out.tx_log_entry.as_ref().unwrap() == &tx_id)
})
.collect::<Vec<OutputData>>();
// 1. Unlock the input by calling cancel_tx
let parent_key_id = w.parent_key_id();
tx::cancel_tx(&mut *w, keychain_mask, &parent_key_id, Some(tx_id), None)?;
if my_contributed_inputs.len() == 0 {
return Ok(None);
}
let input_commit = my_contributed_inputs[0].commit.as_ref().unwrap();
// 2. Create a 1-1 self-spend transaction using this input
let ct_slate = new(
w,
keychain_mask,
&ContractSetupArgsAPI {
// TODO: Check the src_acct_name below. This would use the currently active account
src_acct_name: None,
net_change: Some(0), // self-spend
num_participants: 1,
add_outputs: false,
selection_args: OutputSelectionArgs {
use_inputs: Some(String::from(input_commit)),
..Default::default()
},
proof_args: Default::default(),
},
)?;
let finished_slate = sign(
w,
keychain_mask,
&ct_slate,
&ContractSetupArgsAPI {
// TODO: Check the src_acct_name below. This would use the currently active account
src_acct_name: None,
net_change: None, // we already have it in the context as 0 now
num_participants: 1,
add_outputs: false,
selection_args: OutputSelectionArgs {
use_inputs: Some(String::from(input_commit)),
..Default::default()
},
proof_args: Default::default(),
},
)?;
// TODO: Think about what to do with transaction context of the cancelled slate. It should probably get deleted.
Ok(Some(finished_slate))
}

View file

@ -0,0 +1,93 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of contract setup
use crate::api_impl::owner::check_ttl;
use crate::contract;
use crate::contract::types::ContractSetupArgsAPI;
use crate::error::Error;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate;
use crate::types::{Context, NodeClient, WalletBackend};
/// Perform a contract setup
pub fn setup<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
setup_args: &ContractSetupArgsAPI,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Compute state for 'setup'
let (slate, mut context) = compute(w, keychain_mask, slate, setup_args)?;
// Atomically commit state
contract::utils::save_step(
w,
keychain_mask,
&slate,
&mut context,
setup_args.add_outputs,
false,
)?;
Ok(slate)
}
/// Compute logic for setup
pub fn compute<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
setup_args: &ContractSetupArgsAPI,
) -> Result<(Slate, Context), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut sl = slate.clone();
check_ttl(w, &sl)?;
// Get or create a transaction Context and verify consistency of setup arguments
let mut context = contract::context::get_or_create(w, keychain_mask, &mut sl, setup_args)?;
contract::utils::verify_setup_args_consistency(
&context.setup_args.as_ref().unwrap(),
&setup_args,
)?;
// Add keys and payment proof to slate (both are idempotent operations)
contract::slate::add_keys(&mut sl, &w.keychain(keychain_mask)?, &mut context)?;
contract::slate::add_payment_proof(
w,
&mut sl,
keychain_mask,
&mut context,
&setup_args.net_change,
&setup_args.proof_args,
)?; // noop for the sender
// Add inputs/outputs to the Context if needed. No locking is done here. This happens at save_step.
if setup_args.add_outputs {
contract::context::add_outputs(&mut *w, keychain_mask, &mut context)?;
}
Ok((sl, context))
}

View file

@ -0,0 +1,95 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of contract sign
use crate::contract;
use crate::contract::actions::setup;
use crate::contract::types::ContractSetupArgsAPI;
use crate::error::Error;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate;
use crate::types::{Context, NodeClient, WalletBackend};
/// Sign a contract
pub fn sign<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
setup_args: &ContractSetupArgsAPI,
) -> Result<Slate, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Compute if we will add outputs at this step
let will_add_outputs = match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
Ok(ctx) => ctx.get_inputs().len() + ctx.get_outputs().len() == 0,
Err(_) => true,
};
// Compute state for 'sign'
let (sl, mut context) = compute(w, keychain_mask, slate, setup_args)?;
// Atomically commit state
contract::utils::save_step(w, keychain_mask, &sl, &mut context, will_add_outputs, true)?;
Ok(sl)
}
/// Compute logic for sign
pub fn compute<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
setup_args: &ContractSetupArgsAPI,
) -> Result<(Slate, Context), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut sl = slate.clone();
contract::utils::verify_not_signed(w, sl.id)?;
// Ensure net_change has been provided
let expected_net_change =
contract::utils::get_net_change(w, keychain_mask, &sl, setup_args.net_change)?;
// Define the values that must be provided in the setup phase at the sign step
let mut setup_args = setup_args.clone();
setup_args.net_change = Some(expected_net_change);
setup_args.num_participants = sl.num_participants;
setup_args.add_outputs = true; // we add outputs to the Context in case we haven't done that yet
// Ensure Setup phase is done and that inputs/outputs have been added to the Context
let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?;
// Add outputs to the slate, verify the payment proof and sign the slate
contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?;
if let Some(ref p) = sl.payment_proof {
contract::slate::verify_payment_proof(&sl, expected_net_change, &p.receiver_address)?;
// noop for the receiver
}
contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?;
contract::slate::transition_state(&mut sl)?;
// If we have all the partial signatures, finalize the tx
if contract::slate::can_finalize(&sl) {
contract::slate::finalize(w, keychain_mask, &mut sl)?;
}
Ok((sl, context))
}

View file

@ -0,0 +1,65 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Implementation of contract view
use crate::contract::types::ContractView;
use crate::error::Error;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::slate::{Slate, SlateState};
use crate::types::{NodeClient, WalletBackend};
/// View contract
pub fn view<'a, T: ?Sized, C, K>(
_w: &mut T,
_keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
_encrypted_for: &str,
) -> Result<ContractView, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// NOTE: This should only be run on slates that we received and were signed for us.
// Otherwise, you can't really predict who the party doing the next step should be.
// TODO: Do we need to do any slate verification here?
let suggested_net_change: Option<i64> = match slate.state {
// TODO: Check bounds against overflow/underflow
SlateState::Invoice1 => Some(slate.amount as i64),
SlateState::Standard1 => Some(-(slate.amount as i64)),
_ => None,
};
let is_executed = false;
let num_sigs = slate
.participant_data
.clone()
.into_iter()
.filter(|v| !v.is_complete())
.count();
// TODO: Maybe we can know if the slate was meant for us if it was encrypted for us.
// A possible issue is that one can encrypt the same slate for 10 people.
let ct_view = ContractView {
num_participants: slate.num_participants,
suggested_net_change: suggested_net_change,
agreed_net_change: None, // TODO
num_sigs: num_sigs as u8,
is_executed: is_executed,
..Default::default()
};
Ok(ct_view)
}

View file

@ -0,0 +1,204 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contract functions on the Context
use crate::contract::selection::prepare_outputs;
use crate::contract::types::ContractSetupArgsAPI;
use crate::contract::utils as contract_utils;
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::key::SecretKey;
use crate::internal::{keys, updater};
use crate::slate::Slate;
use crate::types::{Context, NodeClient, WalletBackend};
use crate::{Error, OutputData};
use grin_core::core::FeeFields;
/// Get or create transaction Context for the given slate
pub fn get_or_create<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
setup_args: &ContractSetupArgsAPI,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::context::get_or_create => called");
let maybe_context = w.get_private_context(keychain_mask, slate.id.as_bytes());
let context = match maybe_context {
Err(_) => {
// Get data required for creating a context
let height = w.w2n_client().get_chain_tip()?.0;
let parent_key_id =
contract_utils::parent_key_for(w, setup_args.src_acct_name.as_ref());
self::create(
w,
keychain_mask,
slate,
height,
// &args,
setup_args,
&parent_key_id,
false,
)?
}
Ok(ctx) => ctx,
};
Ok(context)
}
/// Creates a context for a contract
fn create<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
current_height: u64,
// TODO: compare with &InitTxArgs to see if any information is missing
setup_args: &ContractSetupArgsAPI,
parent_key_id: &Identifier,
use_test_rng: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("Creating a new contract context");
// sender should always refresh outputs
updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?;
// Fee contribution estimation
let net_change = setup_args.net_change.unwrap();
// select inputs to estimate fee cost
let (inputs, _, my_fee) =
prepare_outputs(w, &parent_key_id, current_height, &setup_args, None)?;
// The number of outputs we expect is the number of custom outputs plus one change output
debug!(
"My fee contribution estimation: {} for n_inputs: {}, n_outputs: {}, n_kernels: {}, num_participants: {}",
my_fee.fee(), inputs.len(), setup_args.selection_args.num_custom_outputs() + 1, 1, setup_args.num_participants
);
// Make sure `my_fee < net_change` holds for the receiver. This can't be true for a self-spend, because nobody
// has a net_change > 0 which makes a self-spend ok to be a net negative when fees are included.
if net_change > 0 && my_fee.fee() > net_change.abs() as u64 {
panic!(
"My contribution as a receiver would be net negative. my_fee: {}, net_change: {}",
my_fee.fee(),
net_change
);
}
// Add my share of fee contribution to the slate fees
slate.fee_fields = FeeFields::new(0, slate.fee_fields.fee() + my_fee.fee())?;
debug!("Slate.fee: {}", slate.fee_fields.fee());
// Create a Context for this slate
let keychain = w.keychain(keychain_mask)?;
// TODO: it seems 'is_initiator: true' is only used in test_rng. Do we care about this?
let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true);
// Context.fee will hold _our_ fee contribution and not the total slate fee
context.fee = my_fee.as_opt();
// Context.amount is not used in contracts, but we set it anyway.
context.amount = slate.amount;
// TODO: looking at what uses Context.late_lock_args, it seems only the args in SelectionArgs are used except
// for args.ttl_blocks. Is this needed? Can we refactor this?
context.setup_args = Some(setup_args.clone());
debug!(
"Setting Context.net_change as: {}",
context.get_net_change()
);
Ok(context)
}
/// Add outputs to a contract context (including spent outputs which get locked)
pub fn add_outputs<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
context: &mut Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::utils::add_outputs => called");
// Do nothing if we have already contributed our outputs. The assumption is that if this was done,
// our output contribution is complete.
if context.output_ids.len() > 0 || context.input_ids.len() > 0 {
debug!("contract::utils::add_outputs => outputs have already been added, returning.");
return Ok(());
}
let setup_args = context.setup_args.as_ref().unwrap();
debug!("contract::utils::add_outputs => adding outputs");
let current_height = w.w2n_client().get_chain_tip()?.0;
let parent_key_id = &context.parent_key_id;
// Select inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds. Uses committed fee if present.
let (inputs, my_output_amounts, my_fee) = prepare_outputs(
&mut *w,
parent_key_id,
current_height,
&setup_args,
context.fee,
)?;
assert_eq!(my_fee.fee(), context.fee.unwrap().fee(), "my_fee!=ctx.fee");
// Add selected/created inputs/outputs to the context
add_inputs_to_ctx(context, &inputs)?;
add_outputs_to_ctx(w, keychain_mask, context, my_output_amounts)?;
Ok(())
}
/// Add inputs to Context
fn add_inputs_to_ctx(context: &mut Context, inputs: &Vec<OutputData>) -> Result<(), Error> {
debug!("contract::utils::add_inputs_to_ctx => adding inputs to context");
for input in inputs {
context.add_input(&input.key_id, &input.mmr_index, input.value);
debug!(
"contract::utils::add_inputs_to_ctx => input id: {}, value:{}",
&input.key_id, input.value
);
}
Ok(())
}
/// Add outputs to Context
fn add_outputs_to_ctx<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
context: &mut Context,
amounts: Vec<u64>,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
for amount in amounts {
// TODO: it seems like next_available_key does not respect the parent_key_id. Check if it does, it probably should?
// A late-lock might have a different account set to active than the one that was set to the Context
let key_id = keys::next_available_key(w, keychain_mask).unwrap();
context.add_output(&key_id, &None, amount);
debug!(
"contract::utils::add_output_to_ctx => added output to context. Output id: {}, amount: {}",
key_id.clone(),
amount
);
}
Ok(())
}

View file

@ -0,0 +1,28 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This module contains contract related actions.
mod actions;
mod context;
pub mod proofs;
mod selection;
mod slate;
pub mod types;
mod utils;
pub use self::actions::{new, revoke, setup, sign, view};
pub use self::slate::can_finalize;
pub use self::utils::my_fee_contribution;

View file

@ -0,0 +1,452 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Experimental early payment proof functionality, currently only used
//! with contracts. Can move outside of this module if early proofs are adopted
//! by legacy transactions
use crate::contract::types::ProofArgs;
use crate::grin_core::libtx::aggsig;
use crate::grin_core::libtx::secp_ser;
use crate::grin_core::ser as grin_ser;
use crate::grin_core::ser::{Readable, Reader, Writeable, Writer};
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::{PublicKey, SecretKey};
use crate::grin_util::secp::pedersen::Commitment;
use crate::grin_util::secp::Signature;
use crate::grin_util::static_secp_instance;
use crate::slate::{PaymentInfo, PaymentMemo, Slate};
use crate::slate_versions::ser as dalek_ser;
use crate::types::{Context, NodeClient, WalletBackend};
use crate::{address, Error};
use byteorder::{BigEndian, ByteOrder};
use chrono::{DateTime, NaiveDateTime, Utc};
use ed25519_dalek::Keypair as DalekKeypair;
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::SecretKey as DalekSecretKey;
use ed25519_dalek::Signature as DalekSignature;
use ed25519_dalek::{Signer, Verifier};
use grin_util::secp::Message;
use std::convert::TryInto;
/// All elements required to validate a proof within a single struct
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ProofWitness {
/// Kernel index, supplied so verifiers can look up kernel
/// without an expensive lookup operation
#[serde(with = "secp_ser::string_or_u64")]
pub kernel_index: u64,
/// Kernel commitment, supplied so prover can recompute index
/// if required after a reorg
#[serde(
serialize_with = "secp_ser::as_hex",
deserialize_with = "secp_ser::commitment_from_hex"
)]
pub kernel_commitment: Commitment,
/// sender partial signature, used to recover receiver partial signature
#[serde(with = "secp_ser::sig_serde")]
pub sender_partial_sig: Signature,
}
/// Payment proof, to be extracted from slates for
/// signing (when wrapped as PaymentProofBin) or json export from stored tx data
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct InvoiceProof {
/// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce)
pub proof_type: u8,
/// amount
#[serde(with = "secp_ser::string_or_u64")]
pub amount: u64,
/// receiver's public nonce from signing
#[serde(with = "secp_ser::pubkey_serde")]
pub receiver_public_nonce: PublicKey,
/// receiver's public excess from signing
#[serde(with = "secp_ser::pubkey_serde")]
pub receiver_public_excess: PublicKey,
/// Sender's address
#[serde(with = "dalek_ser::dalek_pubkey_serde")]
pub sender_address: DalekPublicKey,
/// Timestamp provided by recipient when signing
pub timestamp: i64,
/// Optional payment memo
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<PaymentMemo>,
/// Not serialized in binary format
#[serde(with = "dalek_ser::option_dalek_sig_serde")]
pub promise_signature: Option<DalekSignature>,
/// Not serialized in binary format, just a convenient place to insert
/// the witness kernel commitment index
#[serde(skip_serializing_if = "Option::is_none")]
pub witness_data: Option<ProofWitness>,
}
struct InvoiceProofBin(InvoiceProof);
impl Writeable for InvoiceProofBin {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_u8(1)?;
// Amount field is 7 bytes, throw error if value is greater
let mut amount_bytes = [0; 8];
BigEndian::write_u64(&mut amount_bytes, self.0.amount);
if amount_bytes[0] > 0 {
return Err(grin_ser::Error::UnexpectedData {
expected: [0u8].to_vec(),
received: [amount_bytes[0]].to_vec(),
});
}
writer.write_fixed_bytes(amount_bytes[1..].to_vec())?;
{
let static_secp = static_secp_instance();
let static_secp = static_secp.lock();
writer.write_fixed_bytes(
self.0
.receiver_public_nonce
.serialize_vec(&static_secp, true),
)?;
writer.write_fixed_bytes(
self.0
.receiver_public_excess
.serialize_vec(&static_secp, true),
)?;
}
writer.write_fixed_bytes(self.0.sender_address.as_bytes())?;
writer.write_i64(self.0.timestamp)?;
match &self.0.memo {
Some(s) => {
writer.write_u8(s.memo_type)?;
writer.write_fixed_bytes(&s.memo.to_vec())?;
}
None => {
writer.write_u8(0)?;
writer.write_fixed_bytes([0u8; 32].to_vec())?;
}
}
Ok(())
}
}
/// Not strictly necessary, but useful for tests
impl Readable for InvoiceProofBin {
fn read<R: Reader>(reader: &mut R) -> Result<InvoiceProofBin, grin_ser::Error> {
// first 8 bytes are proof type + 7 bytes worth of amount
let mut amount = reader.read_u64()?;
let proof_type: u8 = ((amount & 0xFF00_0000_0000_0000) >> 56).try_into().unwrap();
amount &= 0x00FF_FFFF_FFFF_FFFF;
let receiver_public_nonce;
let receiver_public_excess;
{
let static_secp = static_secp_instance();
let static_secp = static_secp.lock();
receiver_public_nonce =
PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?).unwrap();
receiver_public_excess =
PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?).unwrap();
}
let sender_address_vec = reader.read_fixed_bytes(32)?;
let sender_address = DalekPublicKey::from_bytes(&sender_address_vec).unwrap();
let timestamp = reader.read_i64()?;
let memo_type = reader.read_u8()?;
let memo = reader.read_fixed_bytes(32)?;
let mut memo_bytes: [u8; 32] = [0u8; 32];
memo_bytes.copy_from_slice(&memo);
let res = InvoiceProof {
proof_type,
amount,
receiver_public_nonce,
receiver_public_excess,
sender_address,
timestamp,
memo: match memo_type {
0 => None,
_ => Some(PaymentMemo {
memo_type,
memo: memo_bytes,
}),
},
promise_signature: None,
witness_data: None,
};
Ok(InvoiceProofBin(res))
}
}
impl InvoiceProof {
/// Extracts as much data as possible from the slate to create an invoice proof
pub fn from_slate(
slate: &Slate,
participant_index: usize,
sender_address: Option<DalekPublicKey>,
) -> Result<Self, Error> {
// Sender address is either provided or in slate (or error)
let sender_address = match sender_address {
Some(a) => a,
None => {
if let Some(ref p) = slate.payment_proof {
if let Some(a) = p.sender_address {
a
} else {
return Err(Error::NoSenderAddressProvided);
}
} else {
return Err(Error::NoSenderAddressProvided);
}
}
};
let timestamp = match slate.payment_proof.as_ref() {
Some(p) => NaiveDateTime::from_timestamp_opt(p.timestamp.timestamp(), 0)
.unwrap()
.timestamp(),
None => 0,
};
let memo = match slate.payment_proof.as_ref() {
Some(p) => p.memo.clone(),
None => None,
};
let promise_signature = match slate.payment_proof.as_ref() {
Some(p) => p.promise_signature.clone(),
None => None,
};
Ok(Self {
proof_type: 1u8,
amount: slate.amount,
receiver_public_nonce: slate.participant_data[participant_index].public_nonce,
receiver_public_excess: slate.participant_data[participant_index].public_blind_excess,
sender_address,
timestamp,
memo,
promise_signature,
witness_data: None,
})
}
/// Sign the invoice proof, provided all fields are populated
pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> {
let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) {
Ok(k) => k,
Err(e) => {
return Err(Error::ED25519Key(format!("{}", e)));
}
};
let pub_key: DalekPublicKey = (&d_skey).into();
let keypair = DalekKeypair {
public: pub_key,
secret: d_skey,
};
let mut sig_data_bin = Vec::new();
let _ = grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone()))
.expect("serialization failed");
Ok((keypair.sign(&sig_data_bin), pub_key))
}
/// Verify the signature of the invoice proof
pub fn verify_promise_signature(
&self,
recipient_address: &DalekPublicKey,
) -> Result<(), Error> {
if self.promise_signature.is_none() {
return Err(Error::PaymentProofValidation(
"Missing promise signature".into(),
));
}
// Rebuild message
let mut sig_data_bin = Vec::new();
let _ = grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone()))
.expect("serialization failed");
if recipient_address
.verify(&sig_data_bin, self.promise_signature.as_ref().unwrap())
.is_err()
{
return Err(Error::PaymentProof(
"Invalid recipient signature".to_owned(),
));
};
Ok(())
}
/// Verify signature and proof against a given kernel message (kernel lookup is beyond the scope
/// of this module)
pub fn verify_witness(
&self,
recipient_address: &DalekPublicKey,
excess_sig: &Signature,
msg: &Message,
) -> Result<(), Error> {
if self.witness_data.is_none() {
return Err(Error::PaymentProofValidation("Missing witness data".into()));
}
self.verify_promise_signature(recipient_address)?;
let wd = self.witness_data.as_ref().unwrap().clone();
{
let static_secp = static_secp_instance();
let static_secp = static_secp.lock();
let receiver_part_sig =
aggsig::subtract_signature(&static_secp, &excess_sig, &wd.sender_partial_sig)?;
// Retrieve the public nonce sum from the kernel excess signature
let mut pub_nonce_sum_bytes = [3u8; 33];
pub_nonce_sum_bytes[1..33].copy_from_slice(&excess_sig[0..32]);
let pub_nonce_sum = PublicKey::from_slice(&static_secp, &pub_nonce_sum_bytes)?;
// Retrieve the public key sum from the kernel excess
let pub_blind_sum = wd.kernel_commitment.to_pubkey(&static_secp)?;
if let Err(_) = aggsig::verify_partial_sig(
&static_secp,
&receiver_part_sig.0,
&pub_nonce_sum,
&self.receiver_public_excess,
Some(&pub_blind_sum),
&msg,
) {
// Try other possibility
if let Some(s) = receiver_part_sig.1 {
aggsig::verify_partial_sig(
&static_secp,
&s,
&pub_nonce_sum,
&self.receiver_public_excess,
Some(&pub_blind_sum),
&msg,
)?;
} else {
return Err(Error::PaymentProofValidation(
"Signature subtraction failed".into(),
));
}
}
}
Ok(())
}
}
impl serde::Serialize for InvoiceProofBin {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut vec = vec![];
grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self)
.map_err(|err| serde::ser::Error::custom(err.to_string()))?;
serializer.serialize_bytes(&vec)
}
}
/// Adds all info needed for a payment proof to a slate, complete with signed recipient data
pub fn add_payment_proof<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
proof_args: &ProofArgs,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// TODO: Just generating invoice (type 1) for now
let (invoice_proof, promise_signature, receiver_address) =
generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?;
let timestamp = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
let timestamp = DateTime::<Utc>::from_utc(timestamp, Utc);
let proof = PaymentInfo {
sender_address: proof_args.sender_address.clone(),
receiver_address,
timestamp,
promise_signature: Some(promise_signature),
memo: invoice_proof.memo,
};
slate.payment_proof = Some(proof);
Ok(())
}
/// Generates a signature for proof type 'Invoice'
fn generate_invoice_signature<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
proof_args: &ProofArgs,
) -> Result<(InvoiceProof, DalekSignature, DalekPublicKey), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let keychain = wallet.keychain(keychain_mask)?;
let index = slate.find_index_matching_context(&keychain, context)?;
let mut invoice_proof = InvoiceProof::from_slate(&slate, index, proof_args.sender_address)?;
let derivation_index = match context.payment_proof_derivation_index {
Some(i) => i,
None => 0,
};
let parent_key_id = wallet.parent_key_id();
let recp_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
invoice_proof.timestamp = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0)
.unwrap()
.timestamp();
let (sig, addr) = invoice_proof.sign(&recp_key)?;
Ok((invoice_proof, sig, addr))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::slate_versions::tests::populate_test_slate;
#[test]
fn ser_invoice_proof_bin() -> Result<(), Error> {
let mut slate = populate_test_slate()?;
slate.amount |= 0xFF00_0000_0000_0000;
// Bin serialization doesn't include promise sig as it's used to create signature data
slate.payment_proof.as_mut().unwrap().promise_signature = None;
// Should fail, amount too big
let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?;
let mut vec = Vec::new();
assert!(grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof)).is_err());
// Should be okay now
slate.amount = 1234;
let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?;
let mut vec = Vec::new();
grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof.clone()))
.expect("Serialization Failed");
let proof_deser: InvoiceProofBin = grin_ser::deserialize_default(&mut &vec[..]).unwrap();
assert_eq!(invoice_proof, proof_deser.0);
Ok(())
}
}

View file

@ -0,0 +1,596 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contract coin selection functions
use crate::contract::types::{ContractSetupArgsAPI, OutputSelectionArgs};
use crate::contract::utils::my_fee_contribution;
use crate::grin_core::core::amount_to_hr_string;
use crate::grin_keychain::{Identifier, Keychain};
use crate::types::{NodeClient, WalletBackend};
use crate::{Error, OutputData};
use grin_core::core::FeeFields;
/// Prepares inputs & outputs that satisfy `Σmy_inputs >= Σmy_outputs + my_fee_cost` taking into account selection args
pub fn prepare_outputs<'a, T: ?Sized, C, K>(
w: &mut T,
parent_key_id: &Identifier,
current_height: u64,
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
) -> Result<(Vec<OutputData>, Vec<u64>, FeeFields), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Find available inputs
let mut eligible_inputs = find_eligible(w, parent_key_id, current_height)?;
// Select which inputs to use to satisfy the equation
compute(setup_args, committed_fee, &mut eligible_inputs)
}
/// Find all inputs eligible to spend
pub fn find_eligible<'a, T: ?Sized, C, K>(
w: &mut T,
parent_key_id: &Identifier,
current_height: u64,
) -> Result<Vec<OutputData>, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// Find eligible inputs in the wallet
let eligible_inputs = w
.iter()
.filter(|out| out.root_key_id == *parent_key_id && out.eligible_to_spend(current_height, 1))
.collect::<Vec<OutputData>>();
Ok(eligible_inputs)
}
// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use, what amount outputs to make and fee cost
pub fn compute(
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
inputs: &mut Vec<OutputData>,
) -> Result<(Vec<OutputData>, Vec<u64>, FeeFields), Error>
where
{
let (inputs, fee) = select_inputs(setup_args, committed_fee, inputs)?;
let output_amounts = build_output_amount_list(
inputs.clone().iter().map(|out| out.value).sum::<u64>(),
fee.fee(),
setup_args,
);
Ok((inputs, output_amounts, fee))
}
// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use
fn select_inputs(
setup_args: &ContractSetupArgsAPI,
committed_fee: Option<FeeFields>,
inputs: &mut Vec<OutputData>,
) -> Result<(Vec<OutputData>, FeeFields), Error>
where
{
// We use 'lhs' and 'rhs' to denote the amounts on the left/right-hand side of the equation.
// To simulate receive/payment value we:
// - add positive net_change to the 'lhs' for the receiver (to simulate sender's input)
// - add positive net_change to the 'rhs' for the sender (to simulate receiver's output)
// For either party, the following MUST hold for the inputs the function returns:
// Σmy_inputs >= Σmy_outputs + my_fee_cost
// Each party later balances the equation by adding an additional output (change output or receiver output)
let net_change = setup_args.net_change.unwrap();
let custom_outputs_amount_sum = setup_args.selection_args.sum_output_amounts();
let pay_amount = if net_change < 0 {
net_change.abs() as u64
} else {
0
};
// Add the amount we pay and the custom outputs to rhs of the equation
let rhs = pay_amount + custom_outputs_amount_sum;
let required_inputs = setup_args.selection_args.required_inputs();
let is_payjoin = setup_args.selection_args.is_payjoin();
let is_self_spend = setup_args.num_participants == 1;
debug!(
"contract::selection::selecting inputs: num_participants: {}, min_input_amount: {}, is_payjoin: {}",
setup_args.num_participants, rhs, is_payjoin
);
// We don't try to contribute an input only in the case where we have multiple participants
// where we are on the receiving end and we don't want to do a payjoin
if !is_self_spend && (pay_amount == 0 && !is_payjoin) {
return Ok((
vec![],
my_fee_contribution(
0,
setup_args.selection_args.num_custom_outputs() + 1,
1,
setup_args.num_participants,
)?,
));
}
// NOTE: that these are inputs that MUST be selected. We should lock the inputs if they're
// required to minimize any potential race conditions.
let must_use_list = required_inputs.unwrap_or(vec![]);
if must_use_list.len() > 0 {
// Sort the inputs first by the ones listed in the use_inputs and then by value
inputs.sort_by_key(|out| {
(
// We have to negate the boolean to prioritize truthy values because
// false is 0 and hence would be sorted before truthy entries
!(out.commit.is_some()
&& must_use_list.contains(&&out.commit.as_ref().unwrap()[..])),
out.value,
)
});
} else {
// Sort the inputs only by value
inputs.sort_by_key(|out| out.value);
}
// NOTE: Since we sort by value increasingly, if we hold any 0-value inputs, they will all be used if we're the sender
// or a single one if we're the receiver doing a payjoin.
// If we are the receiver, we pretend we have a virtual input from the sender (for which we don't pay the fees) so we can easily
// test that lhs >= rhs and see if we will be able to satisfy equation. We simulate this by starting with lhs = net_change.
let mut lhs = 0;
if net_change > 0 {
lhs = net_change as u64; // TODO: check bounds
}
// We want to count how many inputs we've picked _so far_. This is used to prevent picking
// all 0*H +r*G outputs when we call with min_input_amount=0 and want just a payjoin.
let mut n_inputs = 0;
let mut must_use_list_cnt: u32 = 0;
let my_num_outputs = setup_args.selection_args.num_custom_outputs() + 1;
// If we have already committed to a fee (context.fee) then set this as our "minimum" fee. The reason we have to
// do this is to avoid solving the equation for less than the committed fee. We have to guarantee the inputs we take
// are enough to cover the committed fee. At the end of selection, we check that the fees for the selection were not
// higher than the fee value we committed to.
let mut my_fee = if committed_fee.is_some() {
committed_fee.unwrap()
} else {
// We start with a fee of 1 output and a shared kernel which is minimum for both parties
my_fee_contribution(0, 1, 1, setup_args.num_participants).unwrap()
// FeeFields::zero()
};
// NOTE: This always takes at least one input if it is available. We take the inputs we must take and then we take
// inputs until we fulfill Σmy_inputs >= Σmy_outputs + my_fee_cost
let selected_inputs = inputs
.iter()
.take_while(|out| {
// Take the commitment if it is listed as one of those we MUST take
let must_take =
out.commit.is_some() && must_use_list.contains(&&out.commit.as_ref().unwrap()[..]);
// Compute the fee without this input
let fee_without =
my_fee_contribution(n_inputs, my_num_outputs, 1, setup_args.num_participants)
.unwrap();
// Compute the total fee cost if we took this input
let mut fee_with =
my_fee_contribution(n_inputs + 1, my_num_outputs, 1, setup_args.num_participants)
.unwrap();
// If the current fee is lower than the committed fee (my_fee) then set it to committed fee
if my_fee.fee() > fee_with.fee() {
fee_with = my_fee;
}
// If we don't have a "must take" input, have contributed an input and have enough to balance the equation, we can stop
let can_finish = lhs >= (rhs + fee_without.fee()) && n_inputs > 0 && !must_take;
if can_finish {
return false;
}
// Take the commitment if `lhs < rhs+fees_with` (or if we have not yet taken an input - payjoins)
let should_take = lhs < (rhs + fee_with.fee()) || n_inputs == 0;
let res = must_take || should_take;
if res {
lhs += out.value;
n_inputs += 1;
// Update the fee cost if we decided to take the input
my_fee = fee_with;
if must_take {
must_use_list_cnt += 1;
}
}
debug!(
"contract::selection::select_inputs => out_value:{}, new my_inputs_sum:{}",
out.value, lhs
);
res
})
.cloned()
.collect::<Vec<OutputData>>();
// Return an error if the fee computed is larger than the committed fee
if committed_fee.is_some() && my_fee.fee() > committed_fee.unwrap().fee() {
// TODO: Return a specific Fee estimation error and suggest the user to cancel the transaction
let msg = format!(
"Fee computed ({}) is larger than the committed fee ({})",
my_fee.fee(),
committed_fee.unwrap().fee()
);
return Err(Error::GenericError(msg.into()).into());
}
// Check that the inputs we picked are enough to cover all our output amounts and fees
// asserts that Σmy_inputs >= Σmy_outputs + my_fee_cost
if lhs < rhs + my_fee.fee() {
let total = inputs.iter().fold(0, |acc, x| acc + x.value);
debug!("Not enough funds. Total funds eligible to spend: {}, needed: {}. Fee cost for this transaction: {}", total, rhs+my_fee.fee(), my_fee.fee());
return Err(Error::NotEnoughFunds {
available: total,
available_disp: amount_to_hr_string(total, false),
needed: rhs + my_fee.fee(),
needed_disp: amount_to_hr_string(rhs + my_fee.fee(), false),
}
.into());
// return Err(ErrorKind::GenericError(msg.into()).into());
}
// Assert that all the use_inputs have been selected
if must_use_list.len() != must_use_list_cnt as usize {
let msg = format!(
"We have not found all the inputs that have been requested. {}, found only: {}",
setup_args.selection_args.use_inputs.as_ref().unwrap(),
must_use_list_cnt
);
return Err(Error::GenericError(msg.into()).into());
}
debug!(
"contract::selection::select_inputs => selected_inputs: {:#?}",
selected_inputs
);
// We are returning a set of inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds
Ok((selected_inputs, my_fee))
}
fn build_output_amount_list(
my_input_sum: u64,
my_fee_cost: u64,
setup_args: &ContractSetupArgsAPI,
) -> Vec<u64> {
let expected_net_change = setup_args.net_change.unwrap();
let mut my_output_amounts = setup_args.selection_args.output_amounts();
let custom_outputs_sum = my_output_amounts.iter().sum::<u64>();
// We know that `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds so we balance the equation by adding
// an additional output holding the missing amount (change output or receiver output)
// TODO: check bounds when casting.
let my_change_output_amount =
(my_input_sum - custom_outputs_sum) as i64 + expected_net_change - my_fee_cost as i64;
// TODO: Check if it's even possible for change output to be negative (it shouldn't be if the equation is correct)
if my_change_output_amount < 0 {
panic!(
"contract::selection::build_output_amount_list => ERROR: This should never happen!!! Values: my_input_sum: {}, expected_net_change: {}, my_fee_cost: {}",
my_input_sum as i64, expected_net_change, my_fee_cost as i64
);
}
// Add our change/receiver output (which can be a zero-value output) to the list of outputs
my_output_amounts.push(my_change_output_amount as u64);
debug!(
"contract::selection::build_output_amount_list => inputs sum: {}, my_output_amounts:{:#?}",
my_input_sum, my_output_amounts
);
my_output_amounts
}
/// Compares the output selection args provided at call with those from Context and checks whether they conflict
pub fn verify_selection_consistency(
ctx_args: &OutputSelectionArgs,
cur_args: &OutputSelectionArgs,
) -> Result<(), Error> {
// We can't define a selection strategy if we've already done the setup phase. We only allow to pass either the
// default or exactly the same strategy we defined when doing the setup phase.
// TODO: Test that this works. Perhaps we'd have to define how to compare the two?
if cur_args != ctx_args && cur_args != &OutputSelectionArgs::default() {
panic!("Can't define selection args now because we've already done the setup phase. ctx_selection_args:{:#?}, cur_selection_args:{:#?}", ctx_args, cur_args);
}
// NOTE: The logic above isn't perfect. This is because the user could define arguments that are the default. In this case
// we'd simply silently use the arguments provided in the setup phase. This could be confusing for the user.
Ok(())
}
// Tests
#[cfg(test)]
mod tests {
use super::*;
use crate::grin_keychain::{Identifier, IDENTIFIER_SIZE};
use crate::OutputStatus;
fn _create_output_data_for(amounts: Vec<u64>) -> Vec<OutputData> {
let mut rv: Vec<OutputData> = vec![];
for (idx, amount) in amounts.iter().enumerate() {
let identifier = [0u8; IDENTIFIER_SIZE];
let key_id = Identifier::from_bytes(&identifier);
rv.push(OutputData {
// The identifiers here don't make sense, but they're not needed for testing
root_key_id: key_id.clone(),
key_id: key_id.clone(),
n_child: key_id.clone().to_path().last_path_index(),
mmr_index: None,
commit: Some(format!("{}{}", "abc", idx.to_string())),
value: *amount,
status: OutputStatus::Unspent,
height: 1,
lock_height: 0,
is_coinbase: false,
tx_log_entry: None,
});
}
rv
}
#[test]
fn sender_no_inputs() {
// net_change=-1, no inputs, no fee committed => NotEnoughFunds
let setup_args = ContractSetupArgsAPI {
net_change: Some(-1_000_000_000),
..Default::default()
};
let expected = Error::NotEnoughFunds {
available: 0,
available_disp: amount_to_hr_string(0, false),
needed: 1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(),
needed_disp: amount_to_hr_string(
1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(),
false,
),
};
let result = compute(&setup_args, None, &mut vec![]);
assert_eq!(result.err().unwrap(), expected);
}
#[test]
fn sender_not_enough_funds_for_fee() {
// net_change=-3, inputs=[2, 1], no fee committed => NotEnoughFunds because we can't pay for fees
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let expected = Error::NotEnoughFunds {
available: 3_000_000_000,
available_disp: amount_to_hr_string(3_000_000_000, false),
needed: 3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
needed_disp: amount_to_hr_string(
3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
false,
),
};
let mut inputs = _create_output_data_for(vec![2_000_000_000, 1_000_000_000]);
let result = compute(&setup_args, None, &mut inputs);
assert_eq!(result.err().unwrap(), expected);
}
#[test]
fn sender_happy_path() {
// net_change=-3, inputs=[3, 2, 1, 2], no fee committed => Ok([1, 2, 2], fees)
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let inputs = _create_output_data_for(vec![
3_000_000_000,
2_000_000_000,
1_000_000_000,
2_000_000_000,
]);
// We expect 3 inputs with amounts 1, 2, 2
let expected_inputs = vec![&inputs[2], &inputs[1], &inputs[3]];
let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 5 - 3 - fees
(5_000_000_000 as i64 + (setup_args.net_change.unwrap())) as u64 - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn sender_exact() {
// net_change=-3, inputs=[3, my_fees(2, 1)], no fee committed => Ok([3, my_fees(2, 1)], fees)
let setup_args = ContractSetupArgsAPI {
net_change: Some(-3_000_000_000),
..Default::default()
};
let inputs = _create_output_data_for(vec![
my_fee_contribution(2, 1, 1, 2).unwrap().fee(),
3_000_000_000,
]);
let expected_inputs = inputs.clone(); // we expect both inputs in the same order
let expected_fee = my_fee_contribution(2, 1, 1, 2).unwrap();
let expected_output_amounts = vec![0]; // we expect a change output of 0-value
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn receiver_payjoin_exact() {
// net_change=-my_fees(1, 1), inputs=[my_fees(1, 0)], no fee committed => Ok([3, my_fees(2, 1)], fees)
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(my_fee_contribution(1, 1, 1, 2).unwrap().fee() as i64),
..Default::default()
};
let inputs = _create_output_data_for(vec![0, 1_000_000_000]); // we have a 0-value and 1 grin input
let expected_inputs = vec![&inputs[0]]; // we expect to use the 0-value input
let expected_fee = my_fee_contribution(1, 1, 1, 2).unwrap();
let expected_output_amounts = vec![0]; // we expect a change output of 0-value
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn receiver_no_payjoin() {
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(3_000_000_000),
selection_args: OutputSelectionArgs {
use_inputs: None,
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![1_000_000_000]);
let expected_inputs = vec![];
let expected_fee = my_fee_contribution(0, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 3 - fees
(setup_args.net_change.unwrap() as u64) - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn sender_use_inputs_ok() {
let setup_args = ContractSetupArgsAPI {
// we expect to receive exactly our fee contribution my_fees(1, 1)
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
use_inputs: Some(String::from("abc0,abc2,abc3")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000, // abc0
2_000_000_000,
3_000_000_000, // abc2
4_000_000_000, // abc3
]);
let expected_inputs = vec![&inputs[0], &inputs[2], &inputs[3]];
let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap();
let expected_output_amounts = vec![
// we expect a single output with change holding 3 - fees
(8_000_000_000 as i64 + setup_args.net_change.unwrap()) as u64 - expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
let result_ref = (
result.0.iter().collect::<Vec<&OutputData>>(),
result.1,
result.2,
);
assert_eq!(
result_ref,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
#[test]
fn sender_use_inputs_happy_err() {
let setup_args = ContractSetupArgsAPI {
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
// there is no abc5 input
use_inputs: Some(String::from("abc0,abc2,abc5")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000, // abc0
2_000_000_000,
3_000_000_000, // abc2
4_000_000_000,
]);
let msg = format!(
"We have not found all the inputs that have been requested. abc0,abc2,abc5, found only: 2"
);
let expected_err = Error::GenericError(msg.into()).into();
let result = compute(&setup_args, None, &mut inputs.clone());
assert_eq!(result.err().unwrap(), expected_err);
}
#[test]
fn sender_make_outputs_ok() {
let setup_args = ContractSetupArgsAPI {
net_change: Some(-2_000_000_000),
selection_args: OutputSelectionArgs {
// there is no abc5 input
make_outputs: Some(String::from("1,3")),
..Default::default()
},
..Default::default()
};
let inputs = _create_output_data_for(vec![
1_000_000_000,
2_000_000_000,
3_000_000_000,
4_000_000_000,
]);
// we expect all to be used (2+1+3+fees)
let expected_inputs = inputs.clone();
let expected_fee = my_fee_contribution(4, 3, 1, 2).unwrap();
let expected_output_amounts = vec![
1_000_000_000u64,
3_000_000_000u64,
// change output
((10_000_000_000u64 - 4_000_000_000u64) as i64 + setup_args.net_change.unwrap()) as u64
- expected_fee.fee(),
];
let result = compute(&setup_args, None, &mut inputs.clone()).unwrap();
assert_eq!(
result,
(expected_inputs, expected_output_amounts, expected_fee)
);
}
/*
Tests to add:
- compute_receiver_invariant - test that receiving_amount - my_fees >= 0 (to prevent going into negative accidentally)
- compute_sender_invariant - test that -send_amount - my_fees < 0 (do you need this one and is it correct?)
- compute_receiver_payjoin_negative_fee - could the receiver receive a negative amount through fees? but the thing
would go through because they made a payjoin so they could pay for the fees?
- compute_receiver_omit_payjoin - we can't contribute an input, but have enough for other fees
- compute_make_outputs_fee_err - fail due to not enough funds for fees
- compute_make_outputs_sum_err - is this even possible?
- compute_zero_value_outputs_sender - sender uses all 0-value outputs when sending
- compute_zero_value_inputs_receiver - receiver uses 0-value inputs in payjoin
- test_fee_committed_err - we have already committed to a certain fee which we no longer satisfy
- think if we should have sender/receiver separate testing
- sender_use_all_features
- coinbase output cases
- validate --make-outputs has all positive u64 numbers
*/
}

View file

@ -0,0 +1,269 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contract functions on the Slate
use crate::grin_core::libtx::build;
use crate::grin_core::libtx::proof::ProofBuilder;
use crate::grin_keychain::Keychain;
use crate::grin_util::secp::key::SecretKey;
use crate::slate::{Slate, SlateState};
use crate::types::{Context, NodeClient, WalletBackend};
use crate::Error;
use super::types::ProofArgs;
use crate::contract::proofs::InvoiceProof;
use ed25519_dalek::PublicKey as DalekPublicKey;
/// TODO: Removed for now, consider secp error in sign function
/// The secret key we replace the actual key with after we have signed with the Context keys. This is
/// to prevent possibility of signing with the same key twice.
/// pub const SEC_KEY_FAKE: [u8; 32] = [0; 32];
/// Add payment proof data to slate, noop for sender
pub fn add_payment_proof<'a, T: ?Sized, C, K>(
w: &mut T,
slate: &mut Slate,
keychain_mask: Option<&SecretKey>,
context: &Context,
net_change: &Option<i64>,
proof_args: &ProofArgs,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned
// e.g. slate.add_payment_proof_data()
debug!("contract::slate::add_payment_proof => called");
// If we're a recipient, generate proof unless explicity told not to
if let Some(ref c) = net_change {
if *c > 0 && !proof_args.suppress_proof && slate.payment_proof.is_none() {
super::proofs::add_payment_proof(w, keychain_mask, slate, &context, proof_args)?;
}
}
Ok(())
}
/// Verify payment proof signature
pub fn verify_payment_proof(
slate: &Slate,
net_change: i64,
recipient_address: &DalekPublicKey,
) -> Result<(), Error> {
// TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned
// e.g. slate.verify_payment_proof_sig()
debug!("contract::slate::verify_payment_proof => called");
if net_change > 0 && slate.payment_proof.is_some() {
let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?;
invoice_proof.verify_promise_signature(&recipient_address)?;
}
Ok(())
}
/// Adds inputs and outputs to slate
pub fn add_outputs<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
add_inputs_to_slate(w, keychain_mask, slate, context)?;
add_outputs_to_slate(w, keychain_mask, slate, context)?;
// Adjust the offset for the added input and outputs
let keychain = &w.keychain(keychain_mask)?;
slate.adjust_offset(keychain, &context)?;
Ok(())
}
/// Contribute inputs to slate
fn add_inputs_to_slate<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::slate::add_inputs_to_slate => adding inputs to slate");
let keychain = w.keychain(keychain_mask)?;
let batch = w.batch(keychain_mask)?;
for (key_id, mmr_index, _) in context.get_inputs() {
// We have no information if the input is a coinbase or not, so we fetch the data from DB
let coin = batch.get(&key_id, &mmr_index).unwrap();
if coin.is_coinbase {
slate.add_transaction_elements(
&keychain,
&ProofBuilder::new(&keychain),
vec![build::coinbase_input(coin.value, coin.key_id.clone())],
)?;
debug!(
"contract::slate::add_inputs_to_slate => added coinbase input id: {}, value: {}",
coin.key_id.clone(),
coin.value
);
} else {
slate.add_transaction_elements(
&keychain,
&ProofBuilder::new(&keychain),
vec![build::input(coin.value, coin.key_id.clone())],
)?;
debug!(
"contract::slate::add_inputs_to_slate => added regular input id: {}, value: {}",
coin.key_id.clone(),
coin.value
);
}
}
Ok(())
}
/// Contribute outputs to slate
fn add_outputs_to_slate<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::slate::add_outputs_to_slate => start");
let keychain = w.keychain(keychain_mask)?;
// Iterate over outputs in the Context and add the same output to the slate
for (key_id, _, amount) in context.get_outputs() {
slate.add_transaction_elements(
&keychain,
&ProofBuilder::new(&keychain),
vec![build::output(amount, key_id.clone())],
)?;
debug!(
"contract::slate::add_outputs_to_slate => added output to slate. Output id: {}, amount: {}",
key_id.clone(),
amount
);
}
Ok(())
}
/// Transition the slate state to the next one
pub fn transition_state(slate: &mut Slate) -> Result<(), Error> {
// We don't really use these states right now apart from leaving it to derive expected net_change.
// This suggests these can't be used for manipulation. It doesn't hurt to think a bit more if that's the case.
let new_state = match slate.state {
SlateState::Invoice1 => SlateState::Invoice2,
SlateState::Invoice2 => SlateState::Invoice3,
SlateState::Standard1 => SlateState::Standard2,
SlateState::Standard2 => SlateState::Standard3,
_ => {
debug!("Slate.state: {}", slate.state);
SlateState::Standard3
}
};
slate.state = new_state;
// NOTE: It's possible to never reach the step3. A self-spend has only 2 steps: new -> sign.
Ok(())
}
/// Add partial signature to the slate.
// TODO: Should be a sign & forget pubkey+nonce implementation.
pub fn sign<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
context: &mut Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::slate::sign => called");
let keychain = w.keychain(keychain_mask)?;
slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?;
debug!(
"contract::sign => signed for slate fees: {}",
slate.fee_fields
);
debug!("contract::slate::sign => done");
// TODO: This produces a secp error, probably need a valid key. Verify that this is what we want to do.
// let fake_key = SecretKey::from_slice(keychain.secp(), &SEC_KEY_FAKE)?;
// context.sec_key = fake_key.clone();
// context.sec_nonce = fake_key.clone();
// context.initial_sec_key = fake_key.clone();
// context.initial_sec_nonce = fake_key.clone();
Ok(())
}
/// We can finalize if all partial sigs are present
pub fn can_finalize(slate: &Slate) -> bool {
let res = slate
.participant_data
.clone()
.into_iter()
.filter(|v| !v.is_complete())
.count();
// We can finalize if the number of partial sigs is the same as the number of participants
res == 0 && slate.participant_data.len() == slate.num_participants as usize
}
/// Finalize slate
pub fn finalize<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!("contract::slate::finalize => called");
// Final transaction can be built by anyone at this stage
trace!("Slate to finalize is: {}", slate);
// At this point, everyone adjusted their offset, so we update the offset on the tx
slate.tx_or_err_mut()?.offset = slate.offset.clone();
slate.finalize(&w.keychain(keychain_mask)?)?;
Ok(())
}
/// Perform 'setup' step for a contract. This adds our public key and nonce to the slate
/// The operation should be idempotent.
pub fn add_keys<K>(slate: &mut Slate, keychain: &K, context: &mut Context) -> Result<(), Error>
where
K: Keychain,
{
debug!("contract::slate::add_keys => called");
// TODO: Is this safe from manipulation?
slate.add_participant_info(keychain, context, None)
}

View file

@ -0,0 +1,228 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types related to a contract
use crate::grin_core::consensus;
use crate::slate_versions::ser as dalek_ser;
use ed25519_dalek::PublicKey as DalekPublicKey;
/// Output selection args
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct OutputSelectionArgs {
/// Constraint on how many confirmations used inputs must have
pub min_input_confirmation: u64,
/// Which inputs we want to use - default to payjoin if available with Some("any")
pub use_inputs: Option<String>,
/// Change output specification (comma separated amounts which don't include fee subtraction)
/// e.g. "3,1,4,0,0" describes 5 outputs two of which hold 0 value
pub make_outputs: Option<String>,
}
impl OutputSelectionArgs {
/// We try to make a payjoin if use_inputs has a value (either commitments or Some("any"))
pub fn is_payjoin(&self) -> bool {
self.use_inputs.is_some()
}
/// Return a list of commitments we must use
pub fn required_inputs(&self) -> Option<Vec<&str>> {
if self.use_inputs.is_some() {
Some(
self.use_inputs.as_ref().unwrap()[..]
.split(",")
.filter(|x| *x != "any")
.collect(),
)
} else {
None
}
}
/// Returns the outputs we have to create
pub fn output_amounts(&self) -> Vec<u64> {
if self.make_outputs.is_some() {
let output_amounts: Vec<u64> = self.make_outputs.as_ref().unwrap()[..]
.split(",")
// TODO: move consensus code outside of here. Consider turning make_outputs to Vec<u64>
.map(|amt| (amt.parse::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64)
.collect();
output_amounts
} else {
vec![]
}
}
/// Returns the sum of our output amounts
pub fn sum_output_amounts(&self) -> u64 {
self.output_amounts().iter().sum()
}
/// Returns the number of custom outputs
pub fn num_custom_outputs(&self) -> usize {
self.output_amounts().len()
}
// TODO: make sure to validate this: if custom outputs are specified, it has to be a payjoin.
}
impl Default for OutputSelectionArgs {
fn default() -> OutputSelectionArgs {
OutputSelectionArgs {
min_input_confirmation: 10,
use_inputs: Some(String::from("any")),
make_outputs: None,
}
}
}
/// Types of proof that can be generated
/// as per https://github.com/tromp/grin-rfcs/blob/early-payment-proofs/text/0000-early-payment-proofs.md
/// TODO: Update when RFC is merged
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum ProofType {
/// Legacy (0x00)
Legacy,
/// Invoice (0x01, Default)
Invoice,
/// Sender Nonce (0x02)
SenderNonce,
}
/// Proof generation parameters that can be provided during new or sign phases
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ProofArgs {
/// If net change is positive during this step, whether to suppress the creation of payment proof
pub suppress_proof: bool,
/// Type of proof (Default 'Invoice')
pub proof_type: ProofType,
/// Sender address (required at some stage, may not necessarily be in slate so can be provided explicitly)
#[serde(with = "dalek_ser::option_dalek_pubkey_serde")]
pub sender_address: Option<DalekPublicKey>,
}
impl Default for ProofArgs {
fn default() -> ProofArgs {
ProofArgs {
suppress_proof: false,
proof_type: ProofType::Legacy,
sender_address: None,
}
}
}
/// Contract Setup - defines how we pick inputs/outputs and what we expect from a contract. Both
/// 'new' and 'sign' actions perform a setup phase which is why their endpoints take these parameters.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ContractSetupArgsAPI {
/// The human readable account name from which to draw outputs
/// for the transaction, overriding whatever the active account is as set via the
/// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method.
pub src_acct_name: Option<String>,
/// The net change we will agree on. The amount is in nanogrins (`1 G = 1_000_000_000nG`).
/// The value is positive when we are on the receiving end and negative when we are the sender.
/// It is optional because we could have agreed on it before we reach the sign e.g. when we create new contract
pub net_change: Option<i64>,
/// The number of participants in a contract. Used for computing our kernel fee contribution
pub num_participants: u8,
/// Should we perform an early lock of outputs
pub add_outputs: bool,
/// Output selection arguments
pub selection_args: OutputSelectionArgs,
/// Proof arguments
pub proof_args: ProofArgs,
}
impl Default for ContractSetupArgsAPI {
fn default() -> ContractSetupArgsAPI {
ContractSetupArgsAPI {
src_acct_name: None,
net_change: None,
num_participants: 2,
add_outputs: false,
selection_args: OutputSelectionArgs {
..Default::default()
},
proof_args: ProofArgs::default(),
}
}
}
/// Contract New
#[derive(Clone, Serialize, Deserialize)]
pub struct ContractNewArgsAPI {
/// TODO: do we need the target_slate_version?
/// 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.
pub target_slate_version: Option<u16>,
/// Setup args - contract new also initiates the setup by default
pub setup_args: ContractSetupArgsAPI,
}
impl Default for ContractNewArgsAPI {
fn default() -> ContractNewArgsAPI {
ContractNewArgsAPI {
target_slate_version: None,
setup_args: ContractSetupArgsAPI {
src_acct_name: None,
net_change: None,
num_participants: 2,
add_outputs: false,
selection_args: OutputSelectionArgs {
..Default::default()
},
proof_args: ProofArgs::default(),
},
}
}
}
/// ContractView
#[derive(Clone, Serialize, Deserialize)]
pub struct ContractView {
/// TODO: do we need the target_slate_version?
pub target_slate_version: Option<u16>,
/// Every slatepack has a number of participants
pub num_participants: u8,
/// Suggested value for the party at step2 (only provided if slatepack is at step1)
pub suggested_net_change: Option<i64>,
/// Agreed net_change if we've agreed on it (the context must exist for this)
// NOTE: we drop the Context once we've signed. Perhaps we should think about dropping
// only the private keys associated with it to prevent double-signing with the same
// (pubkey, nonce) pair. This way, we'd retain the history on that wallet instance.
// There might also be value in forgetting the whole context.
pub agreed_net_change: Option<i64>,
/// Number of singatures on the contract
pub num_sigs: u8,
/// Has the contract been executed on chain
pub is_executed: bool,
}
impl Default for ContractView {
fn default() -> ContractView {
ContractView {
target_slate_version: None,
num_participants: 2,
suggested_net_change: None,
agreed_net_change: None,
num_sigs: 0,
is_executed: false,
}
}
}
/// Arguments for contract revoke function
#[derive(Clone, Serialize, Deserialize)]
pub struct ContractRevokeArgsAPI {
/// Tx id to cancel
pub tx_id: u32,
}

View file

@ -0,0 +1,398 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contract building utility functions
use crate::contract::selection::verify_selection_consistency;
use crate::contract::types::ContractSetupArgsAPI;
use crate::grin_core::libtx::tx_fee;
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::secp::key::SecretKey;
use crate::slate::Slate;
use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend};
use crate::util::OnionV3Address;
use crate::{address, Error, OutputData, OutputStatus, TxLogEntry};
use grin_core::core::FeeFields;
use uuid::Uuid;
/// Creates an initial TxLogEntry without input/output or kernel information
pub fn create_tx_log_entry(
slate: &Slate,
net_change: i64,
parent_key_id: Identifier,
log_id: u32,
) -> Result<TxLogEntry, Error> {
let log_type = if slate.num_participants == 1 {
TxLogEntryType::TxSelfSpend
} else {
if net_change > 0 {
TxLogEntryType::TxReceived
} else {
TxLogEntryType::TxSent
}
};
let mut t = TxLogEntry::new(parent_key_id.clone(), log_type, log_id);
// TODO: TxLogEntry has stored_tx field. Check what this needs to be set to and check other fields as well
t.tx_slate_id = Some(slate.id);
if net_change > 0 {
t.amount_credited = net_change as u64;
} else {
t.amount_debited = -net_change as u64;
}
t.ttl_cutoff_height = match slate.ttl_cutoff_height {
0 => None,
n => Some(n),
};
Ok(t)
}
/// Updates TxLogEntry for a contract with information available in the 'sign' step
pub fn update_tx_log_entry<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
context: &Context,
tx_log_entry: &mut TxLogEntry,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// This is expected to be called when we are signing the contract and have already contributed inputs & outputs
let keychain = wallet.keychain(keychain_mask)?;
let parent_key_id = context.parent_key_id.clone();
let current_height = wallet.w2n_client().get_chain_tip()?.0;
// We have already contributed inputs and outputs so we know how much of each we contribute
tx_log_entry.num_outputs = context.output_ids.len();
tx_log_entry.num_inputs = context.input_ids.len();
tx_log_entry.fee = context.fee;
// Set kernel information
match slate.calc_excess(keychain.secp()) {
Ok(e) => tx_log_entry.kernel_excess = Some(e),
Err(_) => panic!("We can't update tx log entry. Excess could not be computed."),
};
tx_log_entry.kernel_lookup_min_height = Some(current_height);
// If we're sending and there's payment proof info in the slate added by recipient, store as well
if let Some(ref p) = slate.payment_proof {
if tx_log_entry.amount_debited > 0 {
// note we only use a single path for now
let sender_address_path = 0u32;
let sender_key = address::address_from_derivation_path(
&keychain,
&parent_key_id,
sender_address_path,
)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
// We're looking for the OTHER party here, the recipient
let sender_index = slate.find_index_matching_context(&keychain, context)?;
let recipient_index = sender_index ^ 1;
tx_log_entry.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.promise_signature,
sender_address: sender_address.to_ed25519()?,
sender_address_path,
sender_signature: None,
/// TODO: Will fill these as separate steps for now, check whether this
/// can be merged in a general case (which means knowing which nonces here belong to
/// the recipient)
proof_type: Some(1u8),
receiver_public_nonce: Some(slate.participant_data[recipient_index].public_nonce),
receiver_public_excess: Some(
slate.participant_data[recipient_index].public_blind_excess,
),
timestamp: Some(p.timestamp),
memo: p.memo.clone(),
promise_signature: p.promise_signature,
sender_part_sig: slate.participant_data[sender_index].part_sig,
});
}
}
Ok(())
}
/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change
pub fn get_net_change<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
// TODO: make this receive only slate.id instead of passing the whole slate
slate: &Slate,
setup_args_net_change: Option<i64>,
) -> Result<i64, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut expected_net_change: Option<i64> = setup_args_net_change;
match w.get_private_context(keychain_mask, slate.id.as_bytes()) {
Ok(ctx) => {
debug!("contract::sign => context found");
// We have a context so we must have agreed on a certain net_change value in Context.net_change.
// If we have both Context.net_change and setup_args.net_change, then they must be equal.
match expected_net_change {
Some(args_net_change) => {
if ctx.get_net_change() != args_net_change {
panic!(
"Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}",
ctx.get_net_change(), args_net_change
);
}
}
None => (),
}
expected_net_change = Some(ctx.get_net_change());
}
Err(_) => debug!("contract::utils::get_net_change => context not found"),
};
// Fail if net_change was not passed to setup_args and was also not present in the context.
// This means it has not been explicitly agreed on and we require the user to pass it.
if expected_net_change.is_none() {
return Err(Error::GenericError(
"You did not agree on the expected net difference.".into(),
)
.into());
}
debug!(
"contract::utils::get_net_change => expected_net_change: {}",
expected_net_change.unwrap()
);
Ok(expected_net_change.unwrap())
}
/// Atomically locks the inputs and saves the changes of Context, TxLogEntry and OutputData.
/// Additionally, the transaction is saved in a file in case we signed it.
pub fn save_step<'a, T: ?Sized, C, K>(
w: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
context: &mut Context,
step_added_outputs: bool,
is_signed: bool,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
debug!(
"contract::utils::save_step => performing atomic update for slate_id: {}",
slate.id
);
// Phase 1 - precompute the data needed for atomic update
let parent_key_id = &context.parent_key_id;
let current_height = w.w2n_client().get_chain_tip()?.0;
// We are at step2 if we don't have context.log_id and we have signed the slate
let is_step2 = !context.log_id.is_some() && is_signed;
let mut tx_log_entry = {
if !context.log_id.is_some() {
// We create a new entry with log_id=0 and but replace it with the real id before committing
create_tx_log_entry(slate, context.get_net_change(), parent_key_id.clone(), 0)?
} else {
w.get_tx_log_entry(parent_key_id.clone(), context.log_id.unwrap())?
.unwrap()
}
};
// Update TxLogEntry if we have signed the contract (we have data about the kernel)
if is_signed {
update_tx_log_entry(w, keychain_mask, &slate, &context, &mut tx_log_entry)?;
// TODO: It's possible to store the transaction in a file while and the atomic commit below fails
// In this case, we should revert to the previous stored tx to avoid having discrepancy
w.store_tx(&format!("{}", slate.id), slate.tx_or_err()?)?;
}
// If we added outputs in this step, we have to create OutputData here because 'batch'
// takes the mutable ref and we can no longer call calc_commit_for_cache for output
let added_outputs = if !step_added_outputs {
vec![]
} else {
let mut output_data_xs: Vec<OutputData> = vec![];
// Create an OutputData entry for every created output
for (key_id, _, amount) in context.get_outputs() {
let commit = w.calc_commit_for_cache(keychain_mask, amount, &key_id)?;
let output_data = OutputData {
root_key_id: parent_key_id.clone(),
key_id: key_id.clone(),
mmr_index: None,
n_child: key_id.to_path().last_path_index(),
commit: commit,
value: amount,
status: OutputStatus::Unconfirmed,
height: current_height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: None,
};
output_data_xs.push(output_data);
}
output_data_xs
};
// Phase 2 - atomically update Context, OutputData and TxLogEntry
let mut batch = w.batch(keychain_mask)?;
// Update TxLogEntry
if !context.log_id.is_some() {
// If we just created the TxLogEntry, we have to assign it an id
let log_id = batch.next_tx_log_id(&parent_key_id)?;
tx_log_entry.id = log_id;
context.log_id = Some(log_id);
}
batch.save_tx_log_entry(tx_log_entry.clone(), &parent_key_id)?;
// Create OutputData entries and lock inputs if we added outputs at this step
if step_added_outputs {
// Create an OutputData entry for every created output
for mut output_data in added_outputs {
output_data.tx_log_entry = context.log_id;
batch.save(output_data)?;
}
// Lock inputs
for id in context.get_inputs() {
let mut coin = batch.get(&id.0, &id.1).unwrap();
// At this point we already have context.log_id set
coin.tx_log_entry = context.log_id;
batch.lock_output(&mut coin)?;
}
}
// Update context
if is_signed && !is_step2 {
// NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps
// when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe.
// The reason we don't delete if we are at step2 is because in case we want to do safe cancel,
// we need to know which inputs are in the context to know which input we have to double-spend.
batch.delete_private_context(slate.id.as_bytes())?;
} else {
batch.save_private_context(slate.id.as_bytes(), &context)?;
}
batch.commit()?;
// TODO: Assert we don't have the context to avoid potentially leaking it! Also write tests around this.
debug!("contract::utils::save_step => Atomic updated done");
Ok(())
}
/// Computes fees contribution for a participant
pub fn my_fee_contribution(
n_inputs: usize,
n_outputs: usize,
n_kernels: usize,
num_participants: u8,
) -> Result<FeeFields, Error> {
// Add our fee costs for our inputs and a single output
let mut fee = tx_fee(n_inputs, n_outputs, 0);
// Add out fee costs for kernel. We pay 1/num_participants of a kernel cost
let kernel_cost = tx_fee(0, 0, n_kernels);
// TODO: we slightly overpay. Make sure to cover all the cases
let my_kernel_cost = (kernel_cost as f64 / (num_participants as f64)).ceil();
fee += my_kernel_cost as u64;
// Add my fee contribution to the slate total fee.
// TODO: Does this break compatibility with existing slates?
let my_fee_fields = FeeFields::new(0, fee)?;
Ok(my_fee_fields)
}
/// Returns an error if the slate has already been signed (in our local database). Even if the
/// result is Ok, it's still possible it was signed but we don't have the data about it locally.
pub fn verify_not_signed<'a, T: ?Sized, C, K>(w: &mut T, slate_id: Uuid) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// If we have a transaction log entry for that slatepack that has a kernel value, then
// we have already signed this slate.
let tx = w
.tx_log_iter()
.find(|t| t.tx_slate_id.is_some() && t.tx_slate_id.unwrap() == slate_id);
let already_signed = tx.is_some() && tx.unwrap().kernel_excess.is_some();
if already_signed {
debug!("contract::utils::verify_not_signed => The slate has already been signed.");
return Err(Error::GenericError(
format!("Slate with id:{} has already been signed.", slate_id).into(),
)
.into());
}
Ok(())
}
/// Compares the setup args provided at call with those in the Context and checks whether they conflict.
/// This is relevant to see if there's any conflict in the arguments provided at step1 with step3.
pub fn verify_setup_args_consistency(
ctx_setup_args: &ContractSetupArgsAPI,
cur_setup_args: &ContractSetupArgsAPI,
) -> Result<(), Error> {
// Compare net_change
if ctx_setup_args.net_change.unwrap() != cur_setup_args.net_change.unwrap() {
panic!(
"Inconsistent net change. Ctx net_change:{}, Current net_change: {}",
ctx_setup_args.net_change.unwrap(),
cur_setup_args.net_change.unwrap()
);
}
// Compare num_participants
if ctx_setup_args.num_participants != cur_setup_args.num_participants {
panic!(
"Inconsistent num_participants. Ctx num_participants:{}, Current num_participants: {}",
ctx_setup_args.num_participants, cur_setup_args.num_participants
);
}
// TODO: Should we verify add_outputs?
// TODO: verify that the parent_key_id is consistent, perhaps even with the active_account set?
// Compare OutputSelectionArgs
verify_selection_consistency(
&ctx_setup_args.selection_args,
&cur_setup_args.selection_args,
)?;
Ok(())
}
/// Get the parent_key_id for a given wallet instance and src_acct_name
pub fn parent_key_for<'a, T: ?Sized, C, K>(w: &mut T, src_acct_name: Option<&String>) -> Identifier
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
// TODO: does it matter what api.set_active_account is set? also check LMDB set_parent_key_id etc. methods
// - Does it matter what api.set_active_account is set? I think w.parent_key_id() already takes the active one
// but the verify_consistency may need to verify this or perhaps give a warning that active is different than
// the one that was set at the first setup phase.
let parent_key_id = match src_acct_name {
Some(d) => {
let pm = w.get_acct_path(d.clone()).unwrap();
match pm {
Some(p) => p.path,
// TODO: should we error if the path is not found?
None => w.parent_key_id(),
}
}
None => w.parent_key_id(),
};
parent_key_id
}

View file

@ -65,6 +65,14 @@ pub enum Error {
#[error("Onion V3 Address Error: {0}")]
OnionV3Address(#[from] util::OnionV3AddressError),
/// Comsig error
#[error("Comsig error: {0}")]
ComSig(#[from] crate::mwmixnet::onion::crypto::comsig::ComSigError),
/// MwMixnet Onion error
#[error("Onion error: {0}")]
Onion(#[from] crate::mwmixnet::onion::onion::OnionError),
/// Callback implementation error conversion
#[error("Trait Implementation error")]
CallbackImpl(&'static str),
@ -257,10 +265,18 @@ pub enum Error {
#[error("Payment Proof parsing error: {0}")]
PaymentProofParsing(String),
/// Retrieving Payment Proof
#[error("Unable to verify payment proof: {0}")]
PaymentProofValidation(String),
/// Decoding OnionV3 addresses to payment proof addresses
#[error("Proof Address decoding: {0}")]
AddressDecoding(String),
/// Payment proof - no sender address provided or found in slate
#[error("Sender address has not been provided")]
NoSenderAddressProvided,
/// Transaction has expired it's TTL
#[error("Transaction Expired")]
TransactionExpired,
@ -309,6 +325,10 @@ pub enum Error {
#[error("Stored Tx error: {0}")]
StoredTx(String),
/// Trying to match index to context
#[error("Cannot match transaction context to slate index")]
ContextToIndex,
/// Other
#[error("Generic error: {0}")]
GenericError(String),

View file

@ -203,12 +203,23 @@ where
sender_address_path,
)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
t.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
receiver_signature: p.promise_signature,
sender_address: sender_address.to_ed25519()?,
sender_address_path,
sender_signature: None,
/// TODO: Will fill these as separate steps for now, check whether this
/// can be merged in a general case (which means knowing which nonces here belong to
/// the recipient)
proof_type: None,
receiver_public_nonce: None,
receiver_public_excess: None,
timestamp: None,
memo: None,
promise_signature: None,
sender_part_sig: None,
});
};

View file

@ -359,12 +359,16 @@ where
Some(&parent_key_id),
false,
)?;
if tx_vec.len() != 1 {
if tx_vec.len() == 0 {
return Err(Error::TransactionDoesntExist(tx_id_string));
}
let tx = tx_vec[0].clone();
for tx in tx_vec {
debug!("cancel_tx: tx: {}", tx.tx_type);
match tx.tx_type {
TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {}
TxLogEntryType::TxSent
| TxLogEntryType::TxReceived
| TxLogEntryType::TxReverted
| TxLogEntryType::TxSelfSpend => {}
_ => return Err(Error::TransactionNotCancellable(tx_id_string)),
}
if tx.confirmed {
@ -380,6 +384,7 @@ where
)?;
let outputs = res.iter().map(|m| m.output.clone()).collect();
updater::cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?;
}
Ok(())
}
@ -421,6 +426,7 @@ where
}
if let Some(ref p) = slate.clone().payment_proof {
if let Some(saddr) = p.sender_address {
let derivation_index = match context.payment_proof_derivation_index {
Some(i) => i,
None => 0,
@ -431,15 +437,25 @@ where
let sender_key =
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
let sender_address = OnionV3Address::from_private(&sender_key.0)?;
let sig =
create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?;
let sig = create_payment_proof_signature(slate.amount, &excess, saddr, sender_key)?;
tx.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address,
receiver_signature: p.receiver_signature,
receiver_signature: p.promise_signature,
sender_address_path: derivation_index,
sender_address: sender_address.to_ed25519()?,
sender_signature: Some(sig),
// Filled in during contract flow proofs for now
proof_type: None,
receiver_public_nonce: None,
receiver_public_excess: None,
timestamp: None,
memo: None,
promise_signature: None,
sender_part_sig: None,
})
} else {
}
}
wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?;
@ -561,11 +577,17 @@ where
let orig_sender_sk =
address::address_from_derivation_path(&keychain, parent_key_id, index)?;
let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?;
if p.sender_address != orig_sender_address.to_ed25519()? {
if let Some(saddr) = p.sender_address {
if saddr != orig_sender_address.to_ed25519()? {
return Err(Error::PaymentProof(
"Sender address on slate does not match original sender address".to_owned(),
));
}
} else {
return Err(Error::PaymentProof(
"Sender address on slate is not provided".to_owned(),
));
}
if orig_proof_info.receiver_address != p.receiver_address {
return Err(Error::PaymentProof(
@ -577,7 +599,7 @@ where
&slate.calc_excess(&keychain.secp())?,
orig_sender_address.to_ed25519()?,
)?;
let sig = match p.receiver_signature {
let sig = match p.promise_signature {
Some(s) => s,
None => {
return Err(Error::PaymentProof(

View file

@ -112,6 +112,7 @@ where
if v {
tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled
&& tx_entry.tx_type != TxLogEntryType::TxSentCancelled
&& tx_entry.tx_type != TxLogEntryType::TxSelfSpendCancelled
} else {
true
}
@ -146,6 +147,7 @@ where
if v {
tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxSentCancelled
|| tx_entry.tx_type == TxLogEntryType::TxSelfSpend
} else {
true
}
@ -158,6 +160,7 @@ where
if v {
tx_entry.tx_type == TxLogEntryType::TxReceived
|| tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled
|| tx_entry.tx_type == TxLogEntryType::TxSelfSpend
} else {
true
}
@ -176,6 +179,17 @@ where
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_self_spend_only {
if v {
tx_entry.tx_type == TxLogEntryType::TxSelfSpend
} else {
true
}
} else {
true
}
})
.filter(|tx_entry| {
if let Some(v) = query_args.include_reverted_only {
if v {
@ -365,7 +379,8 @@ where
!tx_entry.confirmed
&& (tx_entry.tx_type == TxLogEntryType::TxReceived
|| tx_entry.tx_type == TxLogEntryType::TxSent
|| tx_entry.tx_type == TxLogEntryType::TxReverted)
|| tx_entry.tx_type == TxLogEntryType::TxReverted
|| tx_entry.tx_type == TxLogEntryType::TxSelfSpend)
}
false => true,
};
@ -477,6 +492,7 @@ where
TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {
tx.tx_type = TxLogEntryType::TxReceivedCancelled
}
TxLogEntryType::TxSelfSpend => tx.tx_type = TxLogEntryType::TxSelfSpendCancelled,
_ => {}
}
batch.save_tx_log_entry(tx, parent_key_id)?;

View file

@ -33,7 +33,7 @@ use grin_wallet_util as util;
use blake2_rfc as blake2;
#[macro_use]
extern crate serde_derive;
extern crate serde_with;
#[macro_use]
extern crate log;
#[macro_use]
@ -45,11 +45,14 @@ extern crate strum_macros;
pub mod address;
pub mod api_impl;
pub mod contract;
mod error;
mod internal;
mod slate;
pub mod slate_versions;
pub mod slatepack;
pub mod mwmixnet;
mod types;
pub use crate::error::Error;
@ -77,6 +80,8 @@ pub use types::{
WalletOutputBatch,
};
pub use contract::can_finalize;
/// Helper for taking a lock on the wallet instance
#[macro_export]
macro_rules! wallet_lock {

View file

@ -0,0 +1,17 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Onion modules for mxmixnet
pub mod onion;
pub mod types;

View file

@ -0,0 +1,210 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Comsig modules for mxmixnet
use secp256k1zkp::{self, pedersen::Commitment, ContextFlag, Secp256k1, SecretKey};
use blake2_rfc::blake2b::Blake2b;
use byteorder::{BigEndian, ByteOrder};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use secp256k1zkp::rand::thread_rng;
use thiserror::Error;
/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys
#[derive(Clone, Debug)]
pub struct ComSignature {
pub_nonce: Commitment,
s: SecretKey,
t: SecretKey,
}
/// Error types for Commitment Signatures
#[derive(Error, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum ComSigError {
/// Invalid com sig
#[error("Commitment signature is invalid")]
InvalidSig,
/// SECP Error Wrapper
#[error("Secp256k1zkp error: {0:?}")]
Secp256k1zkp(secp256k1zkp::Error),
}
impl From<secp256k1zkp::Error> for ComSigError {
fn from(err: secp256k1zkp::Error) -> ComSigError {
ComSigError::Secp256k1zkp(err)
}
}
impl ComSignature {
/// Create new Com signature from commit and keys
pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature {
ComSignature {
pub_nonce: pub_nonce.to_owned(),
s: s.to_owned(),
t: t.to_owned(),
}
}
#[allow(dead_code)]
/// Sign com signature with kernel values
pub fn sign(
amount: u64,
blind: &SecretKey,
msg: &Vec<u8>,
) -> Result<ComSignature, ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let mut amt_bytes = [0; 32];
BigEndian::write_u64(&mut amt_bytes[24..32], amount);
let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?;
let k_1 = SecretKey::new(&secp, &mut thread_rng());
let k_2 = SecretKey::new(&secp, &mut thread_rng());
let commitment = secp.commit(amount, blind.clone())?;
let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?;
let e = ComSignature::calc_challenge(&secp, &commitment, &nonce_commitment, &msg)?;
// s = k_1 + (e * amount)
let mut s = k_amt.clone();
s.mul_assign(&secp, &e)?;
s.add_assign(&secp, &k_1)?;
// t = k_2 + (e * blind)
let mut t = blind.clone();
t.mul_assign(&secp, &e)?;
t.add_assign(&secp, &k_2)?;
Ok(ComSignature::new(&nonce_commitment, &s, &t))
}
#[allow(non_snake_case)]
/// Verify a com sig
pub fn verify(&self, commit: &Commitment, msg: &Vec<u8>) -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let S1 = secp.commit_blind(self.s.clone(), self.t.clone())?;
let mut Ce = commit.to_pubkey(&secp)?;
let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg)?;
Ce.mul_assign(&secp, &e)?;
let commits = vec![Commitment::from_pubkey(&secp, &Ce)?, self.pub_nonce.clone()];
let S2 = secp.commit_sum(commits, Vec::new())?;
if S1 != S2 {
return Err(ComSigError::InvalidSig);
}
Ok(())
}
fn calc_challenge(
secp: &Secp256k1,
commit: &Commitment,
nonce_commit: &Commitment,
msg: &Vec<u8>,
) -> Result<SecretKey, ComSigError> {
let mut challenge_hasher = Blake2b::new(32);
challenge_hasher.update(&commit.0);
challenge_hasher.update(&nonce_commit.0);
challenge_hasher.update(msg);
let mut challenge = [0; 32];
challenge.copy_from_slice(challenge_hasher.finalize().as_bytes());
Ok(SecretKey::from_slice(&secp, &challenge)?)
}
}
/// Serializes a ComSignature to and from hex
pub mod comsig_serde {
use super::ComSignature;
use grin_core::ser::{self, ProtocolVersion};
use grin_util::ToHex;
use serde::{Deserialize, Serializer};
/// Serializes a ComSignature as a hex string
pub fn serialize<S>(comsig: &ComSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;
let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?;
serializer.serialize_str(&bytes.to_hex())
}
/// Creates a ComSignature from a hex string
pub fn deserialize<'de, D>(deserializer: D) -> Result<ComSignature, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let bytes = String::deserialize(deserializer)
.and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?;
let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?;
Ok(sig)
}
}
#[allow(non_snake_case)]
impl Readable for ComSignature {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let R = Commitment::read(reader)?;
let s = super::secp::read_secret_key(reader)?;
let t = super::secp::read_secret_key(reader)?;
Ok(ComSignature::new(&R, &s, &t))
}
}
impl Writeable for ComSignature {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.pub_nonce.0)?;
writer.write_fixed_bytes(self.s.0)?;
writer.write_fixed_bytes(self.t.0)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey};
use rand::Rng;
use secp256k1zkp::rand::{thread_rng, RngCore};
/// Test signing and verification of ComSignatures
#[test]
fn verify_comsig() -> Result<(), ComSigError> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let amount = thread_rng().next_u64();
let blind = SecretKey::new(&secp, &mut thread_rng());
let msg: [u8; 16] = rand::thread_rng().gen();
let comsig = ComSignature::sign(amount, &blind, &msg.to_vec())?;
let commit = secp.commit(amount, blind.clone())?;
assert!(comsig.verify(&commit, &msg.to_vec()).is_ok());
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err());
let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?;
assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err());
Ok(())
}
}

View file

@ -0,0 +1,293 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Dalek key wrapper for mwmixnet primitives
use super::secp::SecretKey;
use ed25519_dalek::{Keypair, PublicKey, Signature, Signer, Verifier};
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use grin_util::ToHex;
use thiserror::Error;
/// Error types for Dalek structures and logic
#[derive(Clone, Error, Debug, PartialEq)]
pub enum DalekError {
/// Hex deser error
#[error("Hex error {0:?}")]
HexError(String),
/// Key parsing error
#[error("Failed to parse secret key")]
KeyParseError,
/// Error validating signature
#[error("Failed to verify signature")]
SigVerifyFailed,
}
/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization
#[derive(Clone, Debug, PartialEq)]
pub struct DalekPublicKey(PublicKey);
impl DalekPublicKey {
/// Convert DalekPublicKey to hex string
pub fn to_hex(&self) -> String {
self.0.to_hex()
}
/// Convert hex string to DalekPublicKey.
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
let bytes = grin_util::from_hex(hex)
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
let pk = PublicKey::from_bytes(bytes.as_ref())
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
Ok(DalekPublicKey(pk))
}
/// Compute DalekPublicKey from a SecretKey
pub fn from_secret(key: &SecretKey) -> Self {
let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap();
let pk: PublicKey = (&secret).into();
DalekPublicKey(pk)
}
}
impl AsRef<PublicKey> for DalekPublicKey {
fn as_ref(&self) -> &PublicKey {
&self.0
}
}
/// Serializes an Option<DalekPublicKey> to and from hex
pub mod option_dalek_pubkey_serde {
use super::DalekPublicKey;
use grin_util::ToHex;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
///
pub fn serialize<S>(pk: &Option<DalekPublicKey>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match pk {
Some(pk) => serializer.serialize_str(&pk.0.to_hex()),
None => serializer.serialize_none(),
}
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DalekPublicKey>, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).and_then(|res| match res {
Some(string) => DalekPublicKey::from_hex(&string)
.map_err(|e| Error::custom(e.to_string()))
.and_then(|pk: DalekPublicKey| Ok(Some(pk))),
None => Ok(None),
})
}
}
impl Readable for DalekPublicKey {
fn read<R: Reader>(reader: &mut R) -> Result<Self, ser::Error> {
let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?)
.map_err(|_| ser::Error::CorruptedData)?;
Ok(DalekPublicKey(pk))
}
}
impl Writeable for DalekPublicKey {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.0.to_bytes())?;
Ok(())
}
}
/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization
#[derive(Clone, Debug, PartialEq)]
pub struct DalekSignature(Signature);
impl DalekSignature {
/// Convert hex string to DalekSignature.
pub fn from_hex(hex: &str) -> Result<Self, DalekError> {
let bytes = grin_util::from_hex(hex)
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
let sig = Signature::from_bytes(bytes.as_ref())
.map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?;
Ok(DalekSignature(sig))
}
/// Verifies DalekSignature
pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> {
pk.as_ref()
.verify(&msg, &self.0)
.map_err(|_| DalekError::SigVerifyFailed)
}
}
impl AsRef<Signature> for DalekSignature {
fn as_ref(&self) -> &Signature {
&self.0
}
}
/// Serializes a DalekSignature to and from hex
pub mod dalek_sig_serde {
use super::DalekSignature;
use grin_util::ToHex;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
///
pub fn serialize<S>(sig: &DalekSignature, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&sig.0.to_hex())
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<DalekSignature, D::Error>
where
D: Deserializer<'de>,
{
let str = String::deserialize(deserializer)?;
let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?;
Ok(sig)
}
}
/// Dalek signature sign wrapper
// TODO: This is likely duplicated throughout crate, check
pub fn sign(sk: &SecretKey, message: &[u8]) -> Result<DalekSignature, DalekError> {
let secret =
ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?;
let public: PublicKey = (&secret).into();
let keypair = Keypair { secret, public };
let sig = keypair.sign(&message);
Ok(DalekSignature(sig))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mwmixnet::onion::test_util::rand_keypair;
use grin_core::ser::{self, ProtocolVersion};
use grin_util::ToHex;
use rand::Rng;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
struct TestPubKeySerde {
#[serde(with = "option_dalek_pubkey_serde", default)]
pk: Option<DalekPublicKey>,
}
#[test]
fn pubkey_test() -> Result<(), Box<dyn std::error::Error>> {
// Test from_hex
let rand_pk = rand_keypair().1;
let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap();
assert_eq!(rand_pk.0, pk_from_hex.0);
// Test ser (de-)serialization
let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap();
assert_eq!(bytes.len(), 32);
let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap();
assert_eq!(rand_pk.0, pk_from_deser.0);
// Test serde with Some(rand_pk)
let some = TestPubKeySerde {
pk: Some(rand_pk.clone()),
};
let val = serde_json::to_value(some.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::String(s) = o.get("pk").unwrap() {
assert_eq!(s, &rand_pk.0.to_hex());
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(some, serde_json::from_value(val).unwrap());
// Test serde with empty pk field
let none = TestPubKeySerde { pk: None };
let val = serde_json::to_value(none.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::Null = o.get("pk").unwrap() {
// ok
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(none, serde_json::from_value(val).unwrap());
// Test serde with no pk field
let none2 = serde_json::from_str::<TestPubKeySerde>("{}").unwrap();
assert_eq!(none, none2);
Ok(())
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
struct TestSigSerde {
#[serde(with = "dalek_sig_serde")]
sig: DalekSignature,
}
#[test]
fn sig_test() -> Result<(), Box<dyn std::error::Error>> {
// Sign a message
let (sk, pk) = rand_keypair();
let msg: [u8; 16] = rand::thread_rng().gen();
let sig = sign(&sk, &msg).unwrap();
// Verify signature
assert!(sig.verify(&pk, &msg).is_ok());
// Wrong message
let wrong_msg: [u8; 16] = rand::thread_rng().gen();
assert!(sig.verify(&pk, &wrong_msg).is_err());
// Wrong pubkey
let wrong_pk = rand_keypair().1;
assert!(sig.verify(&wrong_pk, &msg).is_err());
// Test from_hex
let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap();
assert_eq!(sig.0, sig_from_hex.0);
// Test serde (de-)serialization
let serde_test = TestSigSerde { sig: sig.clone() };
let val = serde_json::to_value(serde_test.clone()).unwrap();
if let Value::Object(o) = &val {
if let Value::String(s) = o.get("sig").unwrap() {
assert_eq!(s, &sig.0.to_hex());
} else {
panic!("Invalid type");
}
} else {
panic!("Invalid type")
}
assert_eq!(serde_test, serde_json::from_value(val).unwrap());
Ok(())
}
}

View file

@ -0,0 +1,19 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Onion and comsig modules for mxmixnet
pub mod comsig;
pub mod dalek;
pub mod secp;

View file

@ -0,0 +1,79 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! SECP wrapper functions for onion/comsig
//! TODO: Likely redundant stuff in here, trim
pub use secp256k1zkp::aggsig;
pub use secp256k1zkp::constants::{
AGG_SIGNATURE_SIZE, COMPRESSED_PUBLIC_KEY_SIZE, MAX_PROOF_SIZE, PEDERSEN_COMMITMENT_SIZE,
SECRET_KEY_SIZE,
};
pub use secp256k1zkp::ecdh::SharedSecret;
pub use secp256k1zkp::key::{PublicKey, SecretKey, ZERO_KEY};
pub use secp256k1zkp::pedersen::{Commitment, RangeProof};
pub use secp256k1zkp::{ContextFlag, Message, Secp256k1, Signature};
use grin_core::ser::{self, Reader};
use secp256k1zkp::rand::thread_rng;
/// Generate a random SecretKey.
pub fn random_secret() -> SecretKey {
let secp = Secp256k1::new();
SecretKey::new(&secp, &mut thread_rng())
}
/// Deserialize a SecretKey from a Reader
pub fn read_secret_key<R: Reader>(reader: &mut R) -> Result<SecretKey, ser::Error> {
let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?;
let secp = Secp256k1::with_caps(ContextFlag::None);
let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?;
Ok(pk)
}
/// Build a Pedersen Commitment using the provided value and blinding factor
pub fn commit(value: u64, blind: &SecretKey) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let commit = secp.commit(value, blind.clone())?;
Ok(commit)
}
/// Add a blinding factor to an existing Commitment
pub fn add_excess(
commitment: &Commitment,
excess: &SecretKey,
) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let excess_commit: Commitment = secp.commit(0, excess.clone())?;
let commits = vec![commitment.clone(), excess_commit.clone()];
let sum = secp.commit_sum(commits, Vec::new())?;
Ok(sum)
}
/// Subtracts a value (v*H) from an existing commitment
pub fn sub_value(commitment: &Commitment, value: u64) -> Result<Commitment, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Commit);
let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?;
let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?;
Ok(sum)
}
/// Signs the message with the provided SecretKey
pub fn sign(sk: &SecretKey, msg: &Message) -> Result<Signature, secp256k1zkp::Error> {
let secp = Secp256k1::with_caps(ContextFlag::Full);
let pubkey = PublicKey::from_secret_key(&secp, &sk)?;
let sig = aggsig::sign_single(&secp, &msg, &sk, None, None, None, Some(&pubkey), None)?;
Ok(sig)
}

View file

@ -0,0 +1,194 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Onion module definition
pub mod crypto;
pub mod onion;
pub mod util;
use crypto::secp::{random_secret, Commitment, SecretKey};
use onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes};
use chacha20::cipher::StreamCipher;
use grin_core::core::FeeFields;
use secp256k1zkp::pedersen::RangeProof;
use x25519_dalek::PublicKey as xPublicKey;
use x25519_dalek::{SharedSecret, StaticSecret};
/// Onion hop struct
#[derive(Clone)]
pub struct Hop {
/// Comsig server public key
pub server_pubkey: xPublicKey,
/// Kernel excess
pub excess: SecretKey,
/// Fee
pub fee: FeeFields,
/// Rangeproof
pub rangeproof: Option<RangeProof>,
}
/// Crate a new hop
pub fn new_hop(
server_key: &SecretKey,
hop_excess: &SecretKey,
fee: u32,
proof: Option<RangeProof>,
) -> Hop {
Hop {
server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())),
excess: hop_excess.clone(),
fee: FeeFields::from(fee as u32),
rangeproof: proof,
}
}
/// Create an Onion for the Commitment, encrypting the payload for each hop
pub fn create_onion(commitment: &Commitment, hops: &Vec<Hop>) -> Result<Onion, OnionError> {
if hops.is_empty() {
return Ok(Onion {
ephemeral_pubkey: xPublicKey::from([0u8; 32]),
commit: commitment.clone(),
enc_payloads: vec![],
});
}
let mut shared_secrets: Vec<SharedSecret> = Vec::new();
let mut enc_payloads: Vec<RawBytes> = Vec::new();
let mut ephemeral_sk = StaticSecret::from(random_secret().0);
let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk);
for i in 0..hops.len() {
let hop = &hops[i];
let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey);
shared_secrets.push(shared_secret);
ephemeral_sk = StaticSecret::from(random_secret().0);
let next_ephemeral_pk = if i < (hops.len() - 1) {
xPublicKey::from(&ephemeral_sk)
} else {
xPublicKey::from([0u8; 32])
};
let payload = Payload {
next_ephemeral_pk,
excess: hop.excess.clone(),
fee: hop.fee.clone(),
rangeproof: hop.rangeproof.clone(),
};
enc_payloads.push(payload.serialize()?);
}
for i in (0..shared_secrets.len()).rev() {
let mut cipher = new_stream_cipher(&shared_secrets[i])?;
for j in i..shared_secrets.len() {
cipher.apply_keystream(&mut enc_payloads[j]);
}
}
let onion = Onion {
ephemeral_pubkey: onion_ephemeral_pk,
commit: commitment.clone(),
enc_payloads,
};
Ok(onion)
}
/// Internal tests
#[allow(missing_docs)]
pub mod test_util {
use super::*;
use crypto::dalek::DalekPublicKey;
use crypto::secp;
use grin_core::core::hash::Hash;
use grin_util::ToHex;
use rand::{thread_rng, RngCore};
use secp256k1zkp::Secp256k1;
pub fn rand_onion() -> Onion {
let commit = rand_commit();
let mut hops = Vec::new();
let k = (thread_rng().next_u64() % 5) + 1;
for i in 0..k {
let rangeproof = if i == (k - 1) {
Some(rand_proof())
} else {
None
};
let hop = new_hop(
&random_secret(),
&random_secret(),
thread_rng().next_u32(),
rangeproof,
);
hops.push(hop);
}
create_onion(&commit, &hops).unwrap()
}
pub fn rand_commit() -> Commitment {
secp::commit(rand::thread_rng().next_u64(), &secp::random_secret()).unwrap()
}
pub fn rand_hash() -> Hash {
Hash::from_hex(secp::random_secret().to_hex().as_str()).unwrap()
}
pub fn rand_proof() -> RangeProof {
let secp = Secp256k1::new();
secp.bullet_proof(
rand::thread_rng().next_u64(),
secp::random_secret(),
secp::random_secret(),
secp::random_secret(),
None,
None,
)
}
pub fn proof(
value: u64,
fee: u32,
input_blind: &SecretKey,
hop_excesses: &Vec<&SecretKey>,
) -> (Commitment, RangeProof) {
let secp = Secp256k1::new();
let mut blind = input_blind.clone();
for hop_excess in hop_excesses {
blind.add_assign(&secp, &hop_excess).unwrap();
}
let out_value = value - (fee as u64);
let rp = secp.bullet_proof(
out_value,
blind.clone(),
secp::random_secret(),
secp::random_secret(),
None,
None,
);
(secp::commit(out_value, &blind).unwrap(), rp)
}
pub fn rand_keypair() -> (SecretKey, DalekPublicKey) {
let sk = random_secret();
let pk = DalekPublicKey::from_secret(&sk);
(sk, pk)
}
}

View file

@ -0,0 +1,430 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Onion defn for mwmixnet
use super::crypto::secp::{self, Commitment, RangeProof, SecretKey};
use super::util::{read_optional, vec_to_array, write_optional};
use chacha20::cipher::{NewCipher, StreamCipher};
use chacha20::{ChaCha20, Key, Nonce};
use grin_core::core::FeeFields;
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use grin_util::{self, ToHex};
use hmac::digest::InvalidLength;
use hmac::{Hmac, Mac};
use serde::ser::SerializeStruct;
use serde::Deserialize;
use sha2::Sha256;
use std::convert::TryFrom;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::result::Result;
use thiserror::Error;
use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret};
type HmacSha256 = Hmac<Sha256>;
/// Wrap u8 vec
pub type RawBytes = Vec<u8>;
const CURRENT_ONION_VERSION: u8 = 0;
/// A data packet with layers of encryption
#[derive(Clone, Debug)]
pub struct Onion {
/// The onion originator's portion of the shared secret
pub ephemeral_pubkey: xPublicKey,
/// The pedersen commitment before adjusting the excess and subtracting the fee
pub commit: Commitment,
/// The encrypted payloads which represent the layers of the onion
pub enc_payloads: Vec<RawBytes>,
}
impl PartialEq for Onion {
fn eq(&self, other: &Onion) -> bool {
*self.ephemeral_pubkey.as_bytes() == *other.ephemeral_pubkey.as_bytes()
&& self.commit == other.commit
&& self.enc_payloads == other.enc_payloads
}
}
impl Eq for Onion {}
impl Hash for Onion {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write(self.ephemeral_pubkey.as_bytes());
state.write(self.commit.as_ref());
state.write_usize(self.enc_payloads.len());
for p in &self.enc_payloads {
state.write(p.as_slice());
}
}
}
/// A single, decrypted/peeled layer of an Onion.
#[derive(Debug, Clone)]
pub struct Payload {
/// next ephemeral pk
pub next_ephemeral_pk: xPublicKey,
/// excess
pub excess: SecretKey,
/// fee
pub fee: FeeFields,
/// proof
pub rangeproof: Option<RangeProof>,
}
impl Payload {
/// Deser a payload
pub fn deserialize(bytes: &Vec<u8>) -> Result<Payload, ser::Error> {
let payload: Payload = ser::deserialize_default(&mut &bytes[..])?;
Ok(payload)
}
/// Serialize a payload
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
let mut vec = vec![];
ser::serialize_default(&mut vec, &self)?;
Ok(vec)
}
}
impl Readable for Payload {
fn read<R: Reader>(reader: &mut R) -> Result<Payload, ser::Error> {
let version = reader.read_u8()?;
if version != CURRENT_ONION_VERSION {
return Err(ser::Error::UnsupportedProtocolVersion);
}
let next_ephemeral_pk =
xPublicKey::from(vec_to_array::<32>(&reader.read_fixed_bytes(32)?)?);
let excess = secp::read_secret_key(reader)?;
let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?;
let rangeproof = read_optional(reader)?;
Ok(Payload {
next_ephemeral_pk,
excess,
fee,
rangeproof,
})
}
}
impl Writeable for Payload {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_u8(CURRENT_ONION_VERSION)?;
writer.write_fixed_bytes(&self.next_ephemeral_pk.as_bytes())?;
writer.write_fixed_bytes(&self.excess)?;
writer.write_u64(self.fee.into())?;
write_optional(writer, &self.rangeproof)?;
Ok(())
}
}
/// An onion with a layer decrypted
#[derive(Clone, Debug)]
pub struct PeeledOnion {
/// The payload from the peeled layer
pub payload: Payload,
/// The onion remaining after a layer was peeled
pub onion: Onion,
}
impl Onion {
/// Serialize onion
pub fn serialize(&self) -> Result<Vec<u8>, ser::Error> {
let mut vec = vec![];
ser::serialize_default(&mut vec, &self)?;
Ok(vec)
}
/// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload
pub fn peel_layer(&self, server_key: &SecretKey) -> Result<PeeledOnion, OnionError> {
let shared_secret = StaticSecret::from(server_key.0).diffie_hellman(&self.ephemeral_pubkey);
let mut cipher = new_stream_cipher(&shared_secret)?;
let mut decrypted_bytes = self.enc_payloads[0].clone();
cipher.apply_keystream(&mut decrypted_bytes);
let decrypted_payload = Payload::deserialize(&decrypted_bytes)
.map_err(|e| OnionError::DeserializationError(e))?;
let enc_payloads: Vec<RawBytes> = self
.enc_payloads
.iter()
.enumerate()
.filter(|&(i, _)| i != 0)
.map(|(_, enc_payload)| {
let mut p = enc_payload.clone();
cipher.apply_keystream(&mut p);
p
})
.collect();
let mut commitment = self.commit.clone();
commitment = secp::add_excess(&commitment, &decrypted_payload.excess)
.map_err(|e| OnionError::CalcCommitError(e))?;
commitment = secp::sub_value(&commitment, decrypted_payload.fee.into())
.map_err(|e| OnionError::CalcCommitError(e))?;
let peeled_onion = Onion {
ephemeral_pubkey: decrypted_payload.next_ephemeral_pk,
commit: commitment.clone(),
enc_payloads,
};
Ok(PeeledOnion {
payload: decrypted_payload,
onion: peeled_onion,
})
}
}
/// Create new stream cypher from shared secret
pub fn new_stream_cipher(shared_secret: &SharedSecret) -> Result<ChaCha20, OnionError> {
let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?;
mu_hmac.update(shared_secret.as_bytes());
let mukey = mu_hmac.finalize().into_bytes();
let key = Key::from_slice(&mukey[0..32]);
let nonce = Nonce::from_slice(b"NONCE1234567");
Ok(ChaCha20::new(&key, &nonce))
}
impl Writeable for Onion {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
writer.write_fixed_bytes(self.ephemeral_pubkey.as_bytes())?;
writer.write_fixed_bytes(&self.commit)?;
writer.write_u64(self.enc_payloads.len() as u64)?;
for p in &self.enc_payloads {
writer.write_u64(p.len() as u64)?;
p.write(writer)?;
}
Ok(())
}
}
impl Readable for Onion {
fn read<R: Reader>(reader: &mut R) -> Result<Onion, ser::Error> {
let pubkey_bytes: [u8; 32] = vec_to_array(&reader.read_fixed_bytes(32)?)?;
let ephemeral_pubkey = xPublicKey::from(pubkey_bytes);
let commit = Commitment::read(reader)?;
let mut enc_payloads: Vec<RawBytes> = Vec::new();
let len = reader.read_u64()?;
for _ in 0..len {
let size = reader.read_u64()?;
let bytes = reader.read_fixed_bytes(size as usize)?;
enc_payloads.push(bytes);
}
Ok(Onion {
ephemeral_pubkey,
commit,
enc_payloads,
})
}
}
impl serde::ser::Serialize for Onion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut state = serializer.serialize_struct("Onion", 3)?;
state.serialize_field("pubkey", &self.ephemeral_pubkey.as_bytes().to_hex())?;
state.serialize_field("commit", &self.commit.to_hex())?;
let hex_payloads: Vec<String> = self.enc_payloads.iter().map(|v| v.to_hex()).collect();
state.serialize_field("data", &hex_payloads)?;
state.end()
}
}
impl<'de> serde::de::Deserialize<'de> for Onion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Pubkey,
Commit,
Data,
}
struct OnionVisitor;
impl<'de> serde::de::Visitor<'de> for OnionVisitor {
type Value = Onion;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an Onion")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut pubkey = None;
let mut commit = None;
let mut data = None;
while let Some(key) = map.next_key()? {
match key {
Field::Pubkey => {
let val: String = map.next_value()?;
let vec =
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
pubkey =
Some(xPublicKey::from(vec_to_array::<32>(&vec).map_err(
|_| serde::de::Error::custom("Invalid length pubkey"),
)?));
}
Field::Commit => {
let val: String = map.next_value()?;
let vec =
grin_util::from_hex(&val).map_err(serde::de::Error::custom)?;
commit = Some(Commitment::from_vec(vec));
}
Field::Data => {
let val: Vec<String> = map.next_value()?;
let mut vec: Vec<Vec<u8>> = Vec::new();
for hex in val {
vec.push(
grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?,
);
}
data = Some(vec);
}
}
}
Ok(Onion {
ephemeral_pubkey: pubkey.unwrap(),
commit: commit.unwrap(),
enc_payloads: data.unwrap(),
})
}
}
const FIELDS: &[&str] = &["pubkey", "commit", "data"];
deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor)
}
}
/// Error types for creating and peeling Onions
#[derive(Clone, Error, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum OnionError {
/// Invalid Key Length
#[error("Invalid key length for MAC initialization")]
InvalidKeyLength,
/// Serialization error
#[error("Serialization error occurred: {0:?}")]
SerializationError(ser::Error),
/// Deserialization error
#[error("Deserialization error occurred: {0:?}")]
DeserializationError(ser::Error),
/// Error calculating blinding factor
#[error("Error calculating blinding factor: {0:?}")]
CalcBlindError(secp256k1zkp::Error),
/// Error calculating ephemeral key
#[error("Error calculating ephemeral pubkey: {0:?}")]
CalcPubKeyError(secp256k1zkp::Error),
/// Error calculating commitment
#[error("Error calculating commitment: {0:?}")]
CalcCommitError(secp256k1zkp::Error),
}
impl From<InvalidLength> for OnionError {
fn from(_err: InvalidLength) -> OnionError {
OnionError::InvalidKeyLength
}
}
impl From<ser::Error> for OnionError {
fn from(err: ser::Error) -> OnionError {
OnionError::SerializationError(err)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::mwmixnet::onion::crypto::secp::random_secret;
use crate::mwmixnet::onion::{new_hop, Hop};
use grin_core::core::FeeFields;
/// Test end-to-end Onion creation and unwrapping logic.
#[test]
fn onion() {
let total_fee: u64 = 10;
let fee_per_hop: u32 = 2;
let in_value: u64 = 1000;
let out_value: u64 = in_value - total_fee;
let blind = random_secret();
let commitment = secp::commit(in_value, &blind).unwrap();
let mut hops: Vec<Hop> = Vec::new();
let mut keys: Vec<SecretKey> = Vec::new();
let mut final_commit = secp::commit(out_value, &blind).unwrap();
let mut final_blind = blind.clone();
for i in 0..5 {
keys.push(random_secret());
let excess = random_secret();
let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit);
final_blind.add_assign(&secp, &excess).unwrap();
final_commit = secp::add_excess(&final_commit, &excess).unwrap();
let proof = if i == 4 {
let n1 = random_secret();
let rp = secp.bullet_proof(
out_value,
final_blind.clone(),
n1.clone(),
n1.clone(),
None,
None,
);
assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok());
Some(rp)
} else {
None
};
let hop = new_hop(&keys[i], &excess, fee_per_hop, proof);
hops.push(hop);
}
let mut onion_packet = crate::mwmixnet::onion::create_onion(&commitment, &hops).unwrap();
let mut payload = Payload {
next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(),
excess: random_secret(),
fee: FeeFields::from(fee_per_hop as u32),
rangeproof: None,
};
for i in 0..5 {
let peeled = onion_packet.peel_layer(&keys[i]).unwrap();
payload = peeled.payload;
onion_packet = peeled.onion;
}
assert!(payload.rangeproof.is_some());
assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap());
assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit);
assert_eq!(payload.fee, FeeFields::from(fee_per_hop as u32));
}
}

View file

@ -0,0 +1,185 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Util fns for mwmixnet
//! TODO: possibly redundant, check or move elsewhere
use grin_core::ser::{self, Readable, Reader, Writeable, Writer};
use std::convert::TryInto;
/// Writes an optional value as '1' + value if Some, or '0' if None
///
/// This function is used to serialize an optional value into a Writer. If the option
/// contains Some value, it writes '1' followed by the serialized value. If the option
/// is None, it just writes '0'.
///
/// # Arguments
///
/// * `writer` - A Writer instance where the data will be written.
/// * `o` - The Optional value that will be written.
///
/// # Returns
///
/// * If successful, returns Ok with nothing.
/// * If an error occurs during writing, returns Err wrapping the error.
///
/// # Example
///
/// ```
/// use grin_wallet_libwallet::mwmixnet::onion::util::write_optional;
/// let mut writer:Vec<u8> = vec![];
/// let optional_value: Option<u32> = Some(10);
/// //write_optional(&mut writer, &optional_value);
/// ```
pub fn write_optional<O: Writeable, W: Writer>(
writer: &mut W,
o: &Option<O>,
) -> Result<(), ser::Error> {
match &o {
Some(o) => {
writer.write_u8(1)?;
o.write(writer)?;
}
None => writer.write_u8(0)?,
};
Ok(())
}
/// Reads an optional value as '1' + value if Some, or '0' if None
///
/// This function is used to deserialize an optional value from a Reader. If the first byte
/// read is '0', it returns None. If the first byte is '1', it reads the next value and
/// returns Some(value).
///
/// # Arguments
///
/// * `reader` - A Reader instance from where the data will be read.
///
/// # Returns
///
/// * If successful, returns Ok wrapping an optional value. If the first byte read was '0',
/// returns None. If it was '1', returns Some(value).
/// * If an error occurs during reading, returns Err wrapping the error.
///
/// # Example
///
/// ```
/// use grin_wallet_libwallet::mwmixnet::onion::util::read_optional;
/// use grin_core::ser::{BinReader, ProtocolVersion, DeserializationMode};
/// let mut buf: &[u8] = &[1, 0, 0, 0, 10];
/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default());
/// let optional_value: Option<u32> = read_optional(&mut reader).unwrap();
/// assert_eq!(optional_value, Some(10));
/// ```
pub fn read_optional<O: Readable, R: Reader>(reader: &mut R) -> Result<Option<O>, ser::Error> {
let o = if reader.read_u8()? == 0 {
None
} else {
Some(O::read(reader)?)
};
Ok(o)
}
/// Convert a vector to an array of size `S`.
///
/// # Arguments
///
/// * `vec` - The input vector.
///
/// # Returns
///
/// * If successful, returns an `Ok` wrapping an array of size `S` containing
/// the first `S` bytes of `vec`.
/// * If `vec` is smaller than `S`, returns an `Err` indicating a count error.
///
/// # Example
///
/// ```
/// use grin_wallet_libwallet::mwmixnet::onion::util::vec_to_array;
/// let v = vec![0, 1, 2, 3, 4, 5];
/// let a = vec_to_array::<4>(&v).unwrap();
/// assert_eq!(a, [0, 1, 2, 3]);
/// ```
pub fn vec_to_array<const S: usize>(vec: &Vec<u8>) -> Result<[u8; S], ser::Error> {
if vec.len() < S {
return Err(ser::Error::CountError);
}
let arr: [u8; S] = vec[0..S].try_into().unwrap();
Ok(arr)
}
#[cfg(test)]
mod tests {
use super::*;
use grin_core::ser::{BinReader, BinWriter, DeserializationMode, ProtocolVersion};
#[test]
fn test_write_optional() {
// Test with Some value
let mut buf: Vec<u8> = vec![];
let val: Option<u32> = Some(10);
write_optional(&mut BinWriter::default(&mut buf), &val).unwrap();
assert_eq!(buf, &[1, 0, 0, 0, 10]); // 1 for Some, then 10 as a little-endian u32
// Test with None value
buf.clear();
let val: Option<u32> = None;
write_optional(&mut BinWriter::default(&mut buf), &val).unwrap();
assert_eq!(buf, &[0]); // 0 for None
}
#[test]
fn test_read_optional() {
// Test with Some value
let mut buf: &[u8] = &[1, 0, 0, 0, 10]; // 1 for Some, then 10 as a little-endian u32
let val: Option<u32> = read_optional(&mut BinReader::new(
&mut buf,
ProtocolVersion::local(),
DeserializationMode::default(),
))
.unwrap();
assert_eq!(val, Some(10));
// Test with None value
buf = &[0]; // 0 for None
let val: Option<u32> = read_optional(&mut BinReader::new(
&mut buf,
ProtocolVersion::local(),
DeserializationMode::default(),
))
.unwrap();
assert_eq!(val, None);
}
#[test]
fn test_vec_to_array_success() {
let v = vec![1, 2, 3, 4, 5, 6, 7, 8];
let a = vec_to_array::<4>(&v).unwrap();
assert_eq!(a, [1, 2, 3, 4]);
}
#[test]
fn test_vec_to_array_too_small() {
let v = vec![1, 2, 3];
let res = vec_to_array::<4>(&v);
assert!(res.is_err());
}
#[test]
fn test_vec_to_array_empty() {
let v = vec![];
let res = vec_to_array::<4>(&v);
assert!(res.is_err());
}
}

View file

@ -0,0 +1,42 @@
// Copyright 2022 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types related to mwmixnet requests required by rest of lib crate apis
//! Should rexport all needed types here
pub use super::onion::crypto::comsig::{self, ComSignature};
pub use super::onion::crypto::secp::{add_excess, random_secret};
pub use super::onion::onion::Onion;
pub use super::onion::{new_hop, Hop};
use crate::grin_util::secp::key::SecretKey;
use serde::{Deserialize, Serialize};
/// A Swap request
#[derive(Serialize, Deserialize)]
pub struct SwapReq {
/// Com signature
#[serde(with = "comsig::comsig_serde")]
pub comsig: ComSignature,
/// Onion
pub onion: Onion,
}
/// MWMixnetRequest Creation Params
pub struct MixnetReqCreationParams {
/// List of all the server keys
pub server_keys: Vec<SecretKey>,
/// Fees per hop
pub fee_per_hop: u32,
}

View file

@ -28,6 +28,7 @@ use crate::grin_util::secp::key::{PublicKey, SecretKey};
use crate::grin_util::secp::pedersen::Commitment;
use crate::grin_util::secp::Signature;
use crate::grin_util::{secp, static_secp_instance};
use chrono::prelude::{DateTime, NaiveDateTime, Utc};
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::Signature as DalekSignature;
use serde::ser::{Serialize, Serializer};
@ -39,18 +40,37 @@ use crate::slate_versions::v4::{
CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4,
SlateStateV4, SlateV4, VersionCompatInfoV4,
};
use crate::slate_versions::v5::{
CommitsV5, KernelFeaturesArgsV5, OutputFeaturesV5, ParticipantDataV5, PaymentInfoV5,
PaymentMemoV5, SlateStateV5, SlateV5, VersionCompatInfoV5,
};
use crate::slate_versions::VersionedSlate;
use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION};
use crate::Context;
#[derive(Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PaymentMemo {
// The type of memo
// 0x00 is the absence of any specific memo data
// 0x01 is directly embedded additional payment details
// 0x02 represents the blake2b hash of an arbitrary invoice document
pub memo_type: u8,
// memo data itself
pub memo: [u8; 32],
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PaymentInfo {
/// Sender address
pub sender_address: DalekPublicKey,
pub sender_address: Option<DalekPublicKey>,
/// Receiver address
pub receiver_address: DalekPublicKey,
/// Receiver signature
pub receiver_signature: Option<DalekSignature>,
/// Promise signature
pub promise_signature: Option<DalekSignature>,
/// Timestamp (seconds)
pub timestamp: DateTime<Utc>,
/// Memo
pub memo: Option<PaymentMemo>,
}
/// Public data for each participant in the slate
@ -226,10 +246,11 @@ impl Slate {
/// Upgrade a versioned slate
pub fn upgrade(v_slate: VersionedSlate) -> Result<Slate, Error> {
let v4: SlateV4 = match v_slate {
VersionedSlate::V4(s) => s,
let internal: Slate = match v_slate {
VersionedSlate::V4(s) => s.into(),
VersionedSlate::V5(s) => s.into(),
};
Ok(v4.into())
Ok(internal.into())
}
/// Compact the slate for initial sending, storing the excess + offset explicit
/// and removing my input/output data
@ -276,8 +297,12 @@ impl Slate {
kernel_features_args: None,
}
}
/// Removes any signature data that isn't mine, for compacting
/// slates for a return journey
// TODO: Check if this is a noop when we have only 2 parties. The first sig appears at
// step2 and removing everything except your sig means you remove nothing. For more than
// 2 parties, we should probably never remove the part_sigs so that everyone can verify them.
pub fn remove_other_sigdata<K>(
&mut self,
keychain: &K,
@ -310,13 +335,22 @@ impl Slate {
K: Keychain,
B: ProofBuild,
{
debug!("slate::add_transaction_elements => start");
self.update_kernel()?;
debug!("slate::add_transaction_elements => kernel updated");
if elems.is_empty() {
debug!("slate::add_transaction_elements => elems is empty, returning");
return Ok(BlindingFactor::zero());
}
let (tx, blind) =
build::partial_transaction(self.tx_or_err()?.clone(), &elems, keychain, builder)?;
debug!("slate::add_transaction_elements => built partial transaction");
self.tx = Some(tx);
debug!("slate::add_transaction_elements => slate.tx is set");
Ok(blind)
}
@ -377,6 +411,28 @@ impl Slate {
Ok(msg)
}
/// Matches a participant index on the slate with the stored context
pub fn find_index_matching_context<K>(
&self,
keychain: &K,
context: &Context,
) -> Result<usize, Error>
where
K: Keychain,
{
for i in 0..self.num_participants() as usize {
let calc_pub_excess = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?;
let calc_pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?;
// find my entry
if self.participant_data[i].public_blind_excess == calc_pub_excess
|| self.participant_data[i].public_nonce == calc_pub_nonce
{
return Ok(i);
}
}
return Err(Error::ContextToIndex);
}
/// Completes caller's part of round 2, completing signatures
pub fn fill_round_2<K>(
&mut self,
@ -686,13 +742,458 @@ impl Serialize for Slate {
where
S: Serializer,
{
let v4 = SlateV4::from(self);
v4.serialize(serializer)
let v5 = SlateV5::from(self);
v5.serialize(serializer)
}
}
// Current slate version to versioned conversions
// Slate to versioned
////// V5
impl From<Slate> for SlateV5 {
fn from(slate: Slate) -> SlateV5 {
let Slate {
num_participants: num_parts,
id,
state,
tx: _,
amount,
fee_fields,
kernel_features,
ttl_cutoff_height: ttl,
offset: off,
participant_data,
version_info,
payment_proof,
kernel_features_args,
} = slate.clone();
let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data));
let ver = VersionCompatInfoV5::from(&version_info);
let payment_proof = match payment_proof {
Some(p) => Some(PaymentInfoV5::from(&p)),
None => None,
};
let feat_args = match kernel_features_args {
Some(a) => Some(KernelFeaturesArgsV5::from(&a)),
None => None,
};
let sta = SlateStateV5::from(&state);
SlateV5 {
num_parts,
id,
sta,
coms: (&slate).into(),
amt: amount,
fee: fee_fields,
feat: kernel_features,
ttl,
off,
sigs: participant_data,
ver,
proof: payment_proof,
feat_args,
}
}
}
impl From<&Slate> for SlateV5 {
fn from(slate: &Slate) -> SlateV5 {
let Slate {
num_participants: num_parts,
id,
state,
tx: _,
amount,
fee_fields,
kernel_features,
ttl_cutoff_height: ttl,
offset,
participant_data,
version_info,
payment_proof,
kernel_features_args,
} = slate;
let num_parts = *num_parts;
let id = *id;
let amount = *amount;
let fee_fields = *fee_fields;
let feat = *kernel_features;
let ttl = *ttl;
let off = offset.clone();
let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data));
let ver = VersionCompatInfoV5::from(version_info);
let payment_proof = match payment_proof {
Some(p) => Some(PaymentInfoV5::from(p)),
None => None,
};
let sta = SlateStateV5::from(state);
let feat_args = match kernel_features_args {
Some(a) => Some(KernelFeaturesArgsV5::from(a)),
None => None,
};
SlateV5 {
num_parts,
id,
sta,
coms: slate.into(),
amt: amount,
fee: fee_fields,
feat,
ttl,
off,
sigs: participant_data,
ver,
proof: payment_proof,
feat_args,
}
}
}
impl From<&Slate> for Option<Vec<CommitsV5>> {
fn from(slate: &Slate) -> Self {
match slate.tx {
None => None,
Some(ref tx) => {
let mut ret_vec = vec![];
match tx.inputs() {
Inputs::CommitOnly(_) => panic!("commit only inputs unsupported"),
Inputs::FeaturesAndCommit(ref inputs) => {
for input in inputs {
ret_vec.push(input.into());
}
}
}
for output in tx.outputs() {
ret_vec.push(output.into());
}
Some(ret_vec)
}
}
}
}
impl From<&ParticipantData> for ParticipantDataV5 {
fn from(data: &ParticipantData) -> ParticipantDataV5 {
let ParticipantData {
public_blind_excess,
public_nonce,
part_sig,
} = data;
let public_blind_excess = *public_blind_excess;
let public_nonce = *public_nonce;
let part_sig = *part_sig;
ParticipantDataV5 {
xs: public_blind_excess,
nonce: public_nonce,
part: part_sig,
}
}
}
impl From<&SlateState> for SlateStateV5 {
fn from(data: &SlateState) -> SlateStateV5 {
match data {
SlateState::Unknown => SlateStateV5::Unknown,
SlateState::Standard1 => SlateStateV5::Standard1,
SlateState::Standard2 => SlateStateV5::Standard2,
SlateState::Standard3 => SlateStateV5::Standard3,
SlateState::Invoice1 => SlateStateV5::Invoice1,
SlateState::Invoice2 => SlateStateV5::Invoice2,
SlateState::Invoice3 => SlateStateV5::Invoice3,
}
}
}
impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV5 {
fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV5 {
let KernelFeaturesArgs { lock_height } = data;
let lock_hgt = *lock_height;
KernelFeaturesArgsV5 { lock_hgt }
}
}
impl From<&VersionCompatInfo> for VersionCompatInfoV5 {
fn from(data: &VersionCompatInfo) -> VersionCompatInfoV5 {
let VersionCompatInfo {
version,
block_header_version,
} = data;
let version = *version;
let block_header_version = *block_header_version;
VersionCompatInfoV5 {
version,
block_header_version,
}
}
}
impl From<&PaymentInfo> for PaymentInfoV5 {
fn from(data: &PaymentInfo) -> PaymentInfoV5 {
let PaymentInfo {
sender_address,
receiver_address,
promise_signature,
timestamp,
memo,
} = data;
let sender_address = *sender_address;
// TODO: If not provided and we need to downgrade to V5,
// Provide a blank key insted. Consider whether this should fail
// instead, noting that `try_from`isn't currently used in any versioning
// logic
// Also note the zeroized ed25519 public key has a known private key, check if
// this could ever possibly become an issue
let saddr = match sender_address {
Some(a) => a,
None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(),
};
let receiver_address = *receiver_address;
let promise_signature = *promise_signature;
let timestamp = *timestamp;
let memo = match memo {
Some(m) => Some(PaymentMemoV5 {
memo_type: m.memo_type,
memo: m.memo,
}),
None => None,
};
PaymentInfoV5 {
saddr,
raddr: receiver_address,
psig: promise_signature,
ts: timestamp,
memo: memo,
}
}
}
impl From<OutputFeatures> for OutputFeaturesV5 {
fn from(of: OutputFeatures) -> OutputFeaturesV5 {
let index = match of {
OutputFeatures::Plain => 0,
OutputFeatures::Coinbase => 1,
};
OutputFeaturesV5(index)
}
}
///// V5
impl From<SlateV5> for Slate {
fn from(slate: SlateV5) -> Slate {
let SlateV5 {
num_parts: num_participants,
id,
sta,
coms: _,
amt: amount,
fee: fee_fields,
feat: kernel_features,
ttl: ttl_cutoff_height,
off: offset,
sigs: participant_data,
ver,
proof: payment_proof,
feat_args,
} = slate.clone();
let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data));
let version_info = VersionCompatInfo::from(&ver);
let payment_proof = match &payment_proof {
Some(p) => Some(PaymentInfo::from(p)),
None => None,
};
let kernel_features_args = match &feat_args {
Some(a) => Some(KernelFeaturesArgs::from(a)),
None => None,
};
let state = SlateState::from(&sta);
Slate {
num_participants,
id,
state,
tx: (&slate).into(),
amount,
fee_fields,
kernel_features,
ttl_cutoff_height,
offset,
participant_data,
version_info,
payment_proof,
kernel_features_args,
}
}
}
pub fn tx_from_slate_v5(slate: &SlateV5) -> Option<Transaction> {
let coms = match slate.coms.as_ref() {
Some(c) => c,
None => return None,
};
let secp = static_secp_instance();
let secp = secp.lock();
let mut calc_slate = Slate::blank(2, false);
calc_slate.fee_fields = slate.fee;
for d in slate.sigs.iter() {
calc_slate.participant_data.push(ParticipantData {
public_blind_excess: d.xs,
public_nonce: d.nonce,
part_sig: d.part,
});
}
let excess = match calc_slate.calc_excess(&secp) {
Ok(e) => e,
Err(_) => Commitment::from_vec(vec![0]),
};
let excess_sig = match calc_slate.finalize_signature(&secp) {
Ok(s) => s,
Err(_) => Signature::from_raw_data(&[0; 64]).unwrap(),
};
let kernel = TxKernel {
features: match slate.feat {
0 => KernelFeatures::Plain { fee: slate.fee },
1 => KernelFeatures::HeightLocked {
fee: slate.fee,
lock_height: match slate.feat_args.as_ref() {
Some(a) => a.lock_hgt,
None => 0,
},
},
_ => KernelFeatures::Plain { fee: slate.fee },
},
excess,
excess_sig,
};
let mut tx = Slate::empty_transaction().with_kernel(kernel);
let mut outputs = vec![];
let mut inputs = vec![];
for c in coms.iter() {
match &c.p {
Some(p) => {
outputs.push(Output::new(c.f.into(), c.c, p.clone()));
}
None => {
inputs.push(Input {
features: c.f.into(),
commit: c.c,
});
}
}
}
tx.body = tx
.body
.replace_inputs(inputs.as_slice().into())
.replace_outputs(outputs.as_slice());
tx.offset = slate.off.clone();
Some(tx)
}
// Node's Transaction object and lock height to SlateV5 `coms`
impl From<&SlateV5> for Option<Transaction> {
fn from(slate: &SlateV5) -> Option<Transaction> {
tx_from_slate_v5(slate)
}
}
impl From<&ParticipantDataV5> for ParticipantData {
fn from(data: &ParticipantDataV5) -> ParticipantData {
let ParticipantDataV5 {
xs: public_blind_excess,
nonce: public_nonce,
part: part_sig,
} = data;
let public_blind_excess = *public_blind_excess;
let public_nonce = *public_nonce;
let part_sig = *part_sig;
ParticipantData {
public_blind_excess,
public_nonce,
part_sig,
}
}
}
impl From<&KernelFeaturesArgsV5> for KernelFeaturesArgs {
fn from(data: &KernelFeaturesArgsV5) -> KernelFeaturesArgs {
let KernelFeaturesArgsV5 { lock_hgt } = data;
let lock_height = *lock_hgt;
KernelFeaturesArgs { lock_height }
}
}
impl From<&SlateStateV5> for SlateState {
fn from(data: &SlateStateV5) -> SlateState {
match data {
SlateStateV5::Unknown => SlateState::Unknown,
SlateStateV5::Standard1 => SlateState::Standard1,
SlateStateV5::Standard2 => SlateState::Standard2,
SlateStateV5::Standard3 => SlateState::Standard3,
SlateStateV5::Invoice1 => SlateState::Invoice1,
SlateStateV5::Invoice2 => SlateState::Invoice2,
SlateStateV5::Invoice3 => SlateState::Invoice3,
}
}
}
impl From<&VersionCompatInfoV5> for VersionCompatInfo {
fn from(data: &VersionCompatInfoV5) -> VersionCompatInfo {
let VersionCompatInfoV5 {
version,
block_header_version,
} = data;
let version = *version;
let block_header_version = *block_header_version;
VersionCompatInfo {
version,
block_header_version,
}
}
}
impl From<&PaymentInfoV5> for PaymentInfo {
fn from(data: &PaymentInfoV5) -> PaymentInfo {
let PaymentInfoV5 {
saddr: sender_address,
raddr: receiver_address,
psig: promise_signature,
ts: timestamp,
memo,
} = data;
let sender_address = *sender_address;
let receiver_address = *receiver_address;
let promise_signature = *promise_signature;
let timestamp = *timestamp;
let memo: Option<PaymentMemo> = match memo {
Some(m) => {
//memo_ret.copy_from_slice(&grin_util::from_hex(m.memo).unwrap_or_default()[0..32]);
Some(PaymentMemo {
memo_type: m.memo_type,
memo: m.memo,
})
}
None => None,
};
PaymentInfo {
sender_address: Some(sender_address),
receiver_address,
promise_signature: promise_signature,
timestamp,
memo,
}
}
}
impl From<OutputFeaturesV5> for OutputFeatures {
fn from(of: OutputFeaturesV5) -> OutputFeatures {
match of.0 {
1 => OutputFeatures::Coinbase,
0 | _ => OutputFeatures::Plain,
}
}
}
///////// V4
impl From<Slate> for SlateV4 {
fn from(slate: Slate) -> SlateV4 {
let Slate {
@ -875,13 +1376,25 @@ impl From<&PaymentInfo> for PaymentInfoV4 {
let PaymentInfo {
sender_address,
receiver_address,
receiver_signature,
promise_signature: receiver_signature,
timestamp: _,
memo: _,
} = data;
let sender_address = *sender_address;
// TODO: If not provided and we need to downgrade to V4,
// Provide a blank key insted. Consider whether this should fail
// instead, noting that `try_from`isn't currently used in any versioning
// logic
// Also note the zeroized ed25519 public key has a known private key, check if
// this could ever possibly become an issue
let saddr = match sender_address {
Some(a) => a,
None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(),
};
let receiver_address = *receiver_address;
let receiver_signature = *receiver_signature;
PaymentInfoV4 {
saddr: sender_address,
saddr,
raddr: receiver_address,
rsig: receiver_signature,
}
@ -1084,9 +1597,14 @@ impl From<&PaymentInfoV4> for PaymentInfo {
let receiver_address = *receiver_address;
let receiver_signature = *receiver_signature;
PaymentInfo {
sender_address,
sender_address: Some(sender_address),
receiver_address,
receiver_signature,
promise_signature: receiver_signature,
timestamp: DateTime::<Utc>::from_utc(
NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
Utc,
),
memo: None,
}
}
}

View file

@ -20,6 +20,8 @@
use crate::slate::Slate;
use crate::slate_versions::v4::{CoinbaseV4, SlateV4};
use crate::slate_versions::v4_bin::SlateV4Bin;
use crate::slate_versions::v5::{CoinbaseV5, SlateV5};
use crate::slate_versions::v5_bin::SlateV5Bin;
use crate::types::CbData;
use crate::Error;
use std::convert::TryFrom;
@ -30,9 +32,13 @@ pub mod ser;
pub mod v4;
#[allow(missing_docs)]
pub mod v4_bin;
#[allow(missing_docs)]
pub mod v5;
#[allow(missing_docs)]
pub mod v5_bin;
/// The most recent version of the slate
pub const CURRENT_SLATE_VERSION: u16 = 4;
pub const CURRENT_SLATE_VERSION: u16 = 5;
/// The grin block header this slate is intended to be compatible with
pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3;
@ -40,7 +46,9 @@ pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3;
/// Existing versions of the slate
#[derive(EnumIter, Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub enum SlateVersion {
/// V4 (most current)
/// V5 (Most Current)
V5,
/// V4
V4,
}
@ -49,7 +57,9 @@ pub enum SlateVersion {
/// Versions are ordered newest to oldest so serde attempts to
/// deserialize newer versions first, then falls back to older versions.
pub enum VersionedSlate {
/// Current (4.0.0 Onwards )
/// Current (5.0.0 Onwards?)
V5(SlateV5),
/// Current (4.0.0)
V4(SlateV4),
}
@ -57,6 +67,7 @@ impl VersionedSlate {
/// Return slate version
pub fn version(&self) -> SlateVersion {
match *self {
VersionedSlate::V5(_) => SlateVersion::V4,
VersionedSlate::V4(_) => SlateVersion::V4,
}
}
@ -64,6 +75,7 @@ impl VersionedSlate {
/// convert this slate type to a specified older version
pub fn into_version(slate: Slate, version: SlateVersion) -> Result<VersionedSlate, Error> {
match version {
SlateVersion::V5 => Ok(VersionedSlate::V5(slate.into())),
SlateVersion::V4 => Ok(VersionedSlate::V4(slate.into())),
}
}
@ -72,6 +84,7 @@ impl VersionedSlate {
impl From<VersionedSlate> for Slate {
fn from(slate: VersionedSlate) -> Slate {
match slate {
VersionedSlate::V5(s) => Slate::from(s),
VersionedSlate::V4(s) => Slate::from(s),
}
}
@ -81,7 +94,11 @@ impl From<VersionedSlate> for Slate {
#[serde(untagged)]
/// Binary versions, can only be parsed 1:1 into the appropriate
/// version, and VersionedSlate can up/downgrade from there
/// NB (IMPORTANT): Ensure the slates are listed in reverse chronological
/// order (latest first)
pub enum VersionedBinSlate {
/// Version 5, binary
V5(SlateV5Bin),
/// Version 4, binary
V4(SlateV4Bin),
}
@ -90,6 +107,7 @@ impl TryFrom<VersionedSlate> for VersionedBinSlate {
type Error = Error;
fn try_from(slate: VersionedSlate) -> Result<VersionedBinSlate, Error> {
match slate {
VersionedSlate::V5(s) => Ok(VersionedBinSlate::V5(SlateV5Bin(s))),
VersionedSlate::V4(s) => Ok(VersionedBinSlate::V4(SlateV4Bin(s))),
}
}
@ -98,6 +116,7 @@ impl TryFrom<VersionedSlate> for VersionedBinSlate {
impl From<VersionedBinSlate> for VersionedSlate {
fn from(slate: VersionedBinSlate) -> VersionedSlate {
match slate {
VersionedBinSlate::V5(s) => VersionedSlate::V5(s.0),
VersionedBinSlate::V4(s) => VersionedSlate::V4(s.0),
}
}
@ -109,6 +128,8 @@ impl From<VersionedBinSlate> for VersionedSlate {
/// deserialize newer versions first, then falls back to older versions.
pub enum VersionedCoinbase {
/// Current supported coinbase version.
V5(CoinbaseV5),
/// Previous version (no difference)
V4(CoinbaseV4),
}
@ -116,7 +137,196 @@ impl VersionedCoinbase {
/// convert this coinbase data to a specific versioned representation for the json api.
pub fn into_version(cb: CbData, version: SlateVersion) -> VersionedCoinbase {
match version {
SlateVersion::V5 => VersionedCoinbase::V5(cb.into()),
SlateVersion::V4 => VersionedCoinbase::V4(cb.into()),
}
}
}
#[cfg(test)]
pub mod tests {
use crate::grin_core::core::transaction::OutputFeatures;
use crate::grin_util::from_hex;
use crate::grin_util::secp::key::PublicKey;
use crate::grin_util::secp::pedersen::{Commitment, RangeProof};
use crate::grin_util::secp::Signature;
use crate::slate::{KernelFeaturesArgs, ParticipantData, PaymentInfo, PaymentMemo};
use crate::slate_versions::v5::{CommitsV5, SlateV5};
use crate::{
slate, Error, Slate, Slatepacker, SlatepackerArgs, VersionedBinSlate, VersionedSlate,
};
use chrono::{DateTime, NaiveDateTime, Utc};
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::Signature as DalekSignature;
use grin_core::global::{set_local_chain_type, ChainTypes};
use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType};
use std::convert::TryInto;
// Populate a test internal slate with all fields to test conversions
pub fn populate_test_slate() -> Result<Slate, Error> {
let keychain = ExtKeychain::from_random_seed(true).unwrap();
let switch = SwitchCommitmentType::Regular;
let mut slate_internal = Slate::blank(2, false);
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0);
let skey1 = keychain.derive_key(0, &id1, switch).unwrap();
let skey2 = keychain.derive_key(0, &id2, switch).unwrap();
let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap();
let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap();
let part = ParticipantData {
public_blind_excess: xs,
public_nonce: nonce,
part_sig: None,
};
let part2 = ParticipantData {
public_blind_excess: xs,
public_nonce: nonce,
part_sig: Some(Signature::from_raw_data(&[11; 64]).unwrap()),
};
slate_internal.participant_data.push(part.clone());
slate_internal.participant_data.push(part2);
slate_internal.participant_data.push(part);
// Another temp slate to convert commit data into internal 'transaction' like data
// add some random commit data
let slate_tmp = Slate::blank(1, false);
let mut v5 = SlateV5::from(slate_tmp);
let com1 = CommitsV5 {
f: OutputFeatures::Plain.into(),
c: Commitment::from_vec([3u8; 1].to_vec()),
p: None,
};
let com2 = CommitsV5 {
f: OutputFeatures::Plain.into(),
c: Commitment::from_vec([4u8; 1].to_vec()),
p: Some(RangeProof::zero()),
};
let mut coms = vec![];
coms.push(com1.clone());
coms.push(com1.clone());
coms.push(com1.clone());
coms.push(com2);
v5.coms = Some(coms);
slate_internal.tx = slate::tx_from_slate_v5(&v5);
// basic fields
slate_internal.amount = 23820323;
slate_internal.kernel_features = 1;
slate_internal.num_participants = 2;
slate_internal.kernel_features_args = Some(KernelFeaturesArgs {
lock_height: 2323223,
});
// current style payment proof
let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb";
let b = from_hex(raw_pubkey_str).unwrap();
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
// Need to remove milliseconds component for comparison. Won't be serialized
let ts = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
let ts = DateTime::<Utc>::from_utc(ts, Utc);
let pm = PaymentMemo {
memo_type: 1,
memo: [9; 32],
};
let psig = DalekSignature::from_bytes(&[0u8; 64]).unwrap();
slate_internal.payment_proof = Some(PaymentInfo {
sender_address: Some(d_pkey.clone()),
receiver_address: d_pkey.clone(),
timestamp: ts.clone(),
promise_signature: Some(psig),
memo: Some(pm),
});
Ok(slate_internal)
}
#[test]
fn ser_deser_current_slate() -> Result<(), Error> {
let slate_internal = populate_test_slate()?;
// Serialize slate into slatepack
let slatepacker_args = SlatepackerArgs {
sender: None,
recipients: vec![],
dec_key: None,
};
let slate_packer = Slatepacker::new(slatepacker_args);
let slate_packed = slate_packer.create_slatepack(&slate_internal).unwrap();
let slate_unpacked = slate_packer.get_slate(&slate_packed).unwrap();
// Just verifying payment proof for now, extend later to cover EQ for full slate if needs
// be
assert_eq!(slate_internal.payment_proof, slate_unpacked.payment_proof);
Ok(())
}
#[test]
fn slatepack_version_v4_v5() -> Result<(), Error> {
set_local_chain_type(ChainTypes::Mainnet);
// Convert V5 slate into V4 slate, check result
let slate_internal = populate_test_slate()?;
let v5 = VersionedSlate::V5(slate_internal.clone().into());
let v4 = VersionedSlate::V4(slate_internal.into());
let v5_converted: Slate = v5.into();
let v4_converted: Slate = v4.into();
assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some());
// Converted from v4 will not have memos and ts will be zeroed out
assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none());
assert_eq!(
v4_converted
.payment_proof
.as_ref()
.unwrap()
.timestamp
.timestamp(),
0
);
Ok(())
}
#[test]
fn slatepack_version_v4_v5_bin() -> Result<(), Error> {
set_local_chain_type(ChainTypes::Mainnet);
// Convert V5 slate into V4 slate, check result
let slate_internal = populate_test_slate()?;
let v5 = VersionedSlate::V5(slate_internal.clone().into());
let v5_bin: VersionedBinSlate = v5.try_into().unwrap();
let v4 = VersionedSlate::V4(slate_internal.into());
let v4_bin: VersionedBinSlate = v4.try_into().unwrap();
let v5_versioned: VersionedSlate = v5_bin.into();
let v4_versioned: VersionedSlate = v4_bin.into();
let v5_converted: Slate = v5_versioned.into();
let v4_converted: Slate = v4_versioned.into();
assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some());
// Converted from v4 will not have memos and ts will be zeroed out
assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none());
assert_eq!(
v4_converted
.payment_proof
.as_ref()
.unwrap()
.timestamp
.timestamp(),
0
);
Ok(())
}
}

View file

@ -472,7 +472,7 @@ pub mod option_dalek_sig_base64 {
}
}
/// Serializes slates 'version_info' field
/// Serializes slates 'version_info' field - V4
pub mod version_info_v4 {
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
@ -514,7 +514,49 @@ pub mod version_info_v4 {
}
}
/// Serializes slates 'state' field
/// Serializes slates 'version_info' field - V5
pub mod version_info_v5 {
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
use crate::slate_versions::v5::VersionCompatInfoV5;
///
pub fn serialize<S>(v: &VersionCompatInfoV5, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}:{}", v.version, v.block_header_version))
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<VersionCompatInfoV5, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|s| {
let mut retval = VersionCompatInfoV5 {
version: 0,
block_header_version: 0,
};
let v: Vec<&str> = s.split(':').collect();
if v.len() != 2 {
return Err(Error::custom("Cannot parse version"));
}
match u16::from_str_radix(v[0], 10) {
Ok(u) => retval.version = u,
Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))),
}
match u16::from_str_radix(v[1], 10) {
Ok(u) => retval.block_header_version = u,
Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))),
}
Ok(retval)
})
}
}
/// Serializes slates 'state' field - V4
pub mod slate_state_v4 {
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
@ -559,6 +601,51 @@ pub mod slate_state_v4 {
}
}
/// Serializes slates 'state' field - V5
pub mod slate_state_v5 {
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serializer};
use crate::slate_versions::v5::SlateStateV5;
///
pub fn serialize<S>(st: &SlateStateV5, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let label = match st {
SlateStateV5::Unknown => "NA",
SlateStateV5::Standard1 => "S1",
SlateStateV5::Standard2 => "S2",
SlateStateV5::Standard3 => "S3",
SlateStateV5::Invoice1 => "I1",
SlateStateV5::Invoice2 => "I2",
SlateStateV5::Invoice3 => "I3",
};
serializer.serialize_str(label)
}
///
pub fn deserialize<'de, D>(deserializer: D) -> Result<SlateStateV5, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|s| {
let retval = match s.as_str() {
"NA" => SlateStateV5::Unknown,
"S1" => SlateStateV5::Standard1,
"S2" => SlateStateV5::Standard2,
"S3" => SlateStateV5::Standard3,
"I1" => SlateStateV5::Invoice1,
"I2" => SlateStateV5::Invoice2,
"I3" => SlateStateV5::Invoice3,
_ => return Err(Error::custom("Invalid Slate state")),
};
Ok(retval)
})
}
}
/// Serializes an secp256k1 pubkey to base64
pub mod uuid_base64 {
use base64;

View file

@ -64,7 +64,7 @@ impl Readable for SlateStateV4 {
}
/// Allow serializing of Uuids not defined in crate
struct UuidWrap(Uuid);
pub struct UuidWrap(pub Uuid);
impl Writeable for UuidWrap {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
@ -82,7 +82,7 @@ impl Readable for UuidWrap {
}
/// Helper struct to serialize optional fields efficiently
struct SlateOptFields {
pub struct SlateOptFields {
/// num parts, default 2
pub num_parts: u8,
/// amt, default 0

View file

@ -0,0 +1,382 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Contains V5 of the slate (version as yet undetermined)
//!
//! TODO: Should be considered experimental and remain in an experimental branch
//! until such time as the relevant RFCs are accepted
//!
//! Changes from V4:
//! #### Top-Level Slate Struct
//! *
//! #### PaymentInfoV5
//! * `saddr`, i.e. `sender_address` in main Slate becomes optional
//! * `rsig` is renamed to `psig`, corresponding to rename of `receiver_signature` to `promise_signature` in main Slate
//!
//! * `ts` added (`timestamp` in main slate). Serialized as i64 representing epoch time in seconds
//! * `memo` added as optional MemoV5 Struct, which contains:
//! * `memo_type`: u8
//! * 0x00 = payment details directly embedded
//! * 0x01 = Blake2b hash of an arbitrary invoice document
//! * `memo`: [u8;32] the memo data itself
use crate::grin_core::core::FeeFields;
use crate::grin_core::core::{Input, Output, TxKernel};
use crate::grin_core::libtx::secp_ser;
use crate::grin_keychain::{BlindingFactor, Identifier};
use crate::grin_util::secp;
use crate::grin_util::secp::key::PublicKey;
use crate::grin_util::secp::pedersen::{Commitment, RangeProof};
use crate::grin_util::secp::Signature;
use crate::{slate_versions::ser, CbData};
use chrono::prelude::{DateTime, Utc};
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::Signature as DalekSignature;
use serde_with::TimestampSeconds;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SlateV5 {
// Required Fields
/// Versioning info
#[serde(with = "ser::version_info_v5")]
pub ver: VersionCompatInfoV5,
/// Unique transaction ID, selected by sender
pub id: Uuid,
/// Slate state
#[serde(with = "ser::slate_state_v5")]
pub sta: SlateStateV5,
/// Offset, modified by each participant inserting inputs
/// as the transaction progresses
#[serde(
serialize_with = "secp_ser::as_hex",
deserialize_with = "secp_ser::blind_from_hex"
)]
#[serde(default = "default_offset_zero")]
#[serde(skip_serializing_if = "offset_is_zero")]
pub off: BlindingFactor,
// Optional fields depending on state
/// The number of participants intended to take part in this transaction
#[serde(default = "default_num_participants_2")]
#[serde(skip_serializing_if = "num_parts_is_2")]
pub num_parts: u8,
/// base amount (excluding fee)
#[serde(with = "secp_ser::string_or_u64")]
#[serde(skip_serializing_if = "u64_is_blank")]
#[serde(default = "default_u64_0")]
pub amt: u64,
/// fee
#[serde(skip_serializing_if = "fee_is_zero")]
#[serde(default = "default_fee")]
pub fee: FeeFields,
/// kernel features, if any
#[serde(skip_serializing_if = "u8_is_blank")]
#[serde(default = "default_u8_0")]
pub feat: u8,
/// TTL, the block height at which wallets
/// should refuse to process the transaction and unlock all
#[serde(with = "secp_ser::string_or_u64")]
#[serde(skip_serializing_if = "u64_is_blank")]
#[serde(default = "default_u64_0")]
pub ttl: u64,
// Structs always required
/// Participant data, each participant in the transaction will
/// insert their public data here. For now, 0 is sender and 1
/// is receiver, though this will change for multi-party
pub sigs: Vec<ParticipantDataV5>,
// Situational, but required at some point in the tx
/// Inputs/Output commits added to slate
#[serde(default = "default_coms_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub coms: Option<Vec<CommitsV5>>,
// Optional Structs
/// Payment Proof
#[serde(default = "default_payment_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<PaymentInfoV5>,
/// Kernel features arguments
#[serde(default = "default_kernel_features_none")]
#[serde(skip_serializing_if = "Option::is_none")]
pub feat_args: Option<KernelFeaturesArgsV5>,
}
fn default_payment_none() -> Option<PaymentInfoV5> {
None
}
fn default_offset_zero() -> BlindingFactor {
BlindingFactor::zero()
}
fn offset_is_zero(o: &BlindingFactor) -> bool {
*o == BlindingFactor::zero()
}
fn default_coms_none() -> Option<Vec<CommitsV5>> {
None
}
fn default_u64_0() -> u64 {
0
}
fn num_parts_is_2(n: &u8) -> bool {
*n == 2
}
fn default_num_participants_2() -> u8 {
2
}
fn default_kernel_features_none() -> Option<KernelFeaturesArgsV5> {
None
}
/// Slate state definition
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum SlateStateV5 {
/// Unknown, coming from earlier versions of the slate
Unknown,
/// Standard flow, freshly init
Standard1,
/// Standard flow, return journey
Standard2,
/// Standard flow, ready for transaction posting
Standard3,
/// Invoice flow, freshly init
Invoice1,
///Invoice flow, return journey
Invoice2,
/// Invoice flow, ready for tranasction posting
Invoice3,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
/// Kernel features arguments definition
pub struct KernelFeaturesArgsV5 {
/// Lock height, for HeightLocked
pub lock_hgt: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct VersionCompatInfoV5 {
/// The current version of the slate format
pub version: u16,
/// Version of grin block header this slate is compatible with
pub block_header_version: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ParticipantDataV5 {
/// Public key corresponding to private blinding factor
#[serde(with = "secp_ser::pubkey_serde")]
pub xs: PublicKey,
/// Public key corresponding to private nonce
#[serde(with = "secp_ser::pubkey_serde")]
pub nonce: PublicKey,
/// Public partial signature
#[serde(default = "default_part_sig_none")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "secp_ser::option_sig_serde")]
pub part: Option<Signature>,
}
fn default_part_sig_none() -> Option<Signature> {
None
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PaymentMemoV5 {
// The type of memo
// 0x00 is directly embedded additional payment details
// 0x01 represents the blake2b hash of an arbitrary invoice document
pub memo_type: u8,
// memo data itself
pub memo: [u8; 32],
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct PaymentInfoV5 {
#[serde(with = "ser::dalek_pubkey_serde")]
pub saddr: DalekPublicKey,
#[serde(with = "ser::dalek_pubkey_serde")]
pub raddr: DalekPublicKey,
#[serde_as(as = "TimestampSeconds<i64>")]
pub ts: DateTime<Utc>,
#[serde(default = "default_promise_signature_none")]
#[serde(with = "ser::option_dalek_sig_serde")]
#[serde(skip_serializing_if = "Option::is_none")]
pub psig: Option<DalekSignature>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<PaymentMemoV5>,
}
fn default_promise_signature_none() -> Option<DalekSignature> {
None
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct CommitsV5 {
/// Options for an output's structure or use
#[serde(default = "default_output_feature")]
#[serde(skip_serializing_if = "output_feature_is_plain")]
pub f: OutputFeaturesV5,
/// The homomorphic commitment representing the output amount
#[serde(
serialize_with = "secp_ser::as_hex",
deserialize_with = "secp_ser::commitment_from_hex"
)]
pub c: Commitment,
/// A proof that the commitment is in the right range
/// Only applies for transaction outputs
#[serde(with = "ser::option_rangeproof_hex")]
#[serde(default = "default_range_proof")]
#[serde(skip_serializing_if = "Option::is_none")]
pub p: Option<RangeProof>,
}
impl From<&Output> for CommitsV5 {
fn from(out: &Output) -> CommitsV5 {
CommitsV5 {
f: out.features().into(),
c: out.commitment(),
p: Some(out.proof()),
}
}
}
// This will need to be reworked once we no longer support input features with "commit only" inputs.
impl From<&Input> for CommitsV5 {
fn from(input: &Input) -> CommitsV5 {
CommitsV5 {
f: input.features.into(),
c: input.commitment(),
p: None,
}
}
}
fn default_output_feature() -> OutputFeaturesV5 {
OutputFeaturesV5(0)
}
fn output_feature_is_plain(o: &OutputFeaturesV5) -> bool {
o.0 == 0
}
#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq, Eq)]
pub struct OutputFeaturesV5(pub u8);
pub fn sig_is_blank(s: &secp::Signature) -> bool {
for b in s.to_raw_data().iter() {
if *b != 0 {
return false;
}
}
true
}
fn default_range_proof() -> Option<RangeProof> {
None
}
fn u64_is_blank(u: &u64) -> bool {
*u == 0
}
fn default_u8_0() -> u8 {
0
}
fn u8_is_blank(u: &u8) -> bool {
*u == 0
}
fn fee_is_zero(f: &FeeFields) -> bool {
f.is_zero()
}
fn default_fee() -> FeeFields {
FeeFields::zero()
}
/// A mining node requests new coinbase via the foreign api every time a new candidate block is built.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CoinbaseV5 {
/// Output
output: CbOutputV5,
/// Kernel
kernel: CbKernelV5,
/// Key Id
key_id: Option<Identifier>,
}
impl From<CbData> for CoinbaseV5 {
fn from(cb: CbData) -> CoinbaseV5 {
CoinbaseV5 {
output: CbOutputV5::from(&cb.output),
kernel: CbKernelV5::from(&cb.kernel),
key_id: cb.key_id,
}
}
}
impl From<&Output> for CbOutputV5 {
fn from(output: &Output) -> CbOutputV5 {
CbOutputV5 {
features: CbOutputFeatures::Coinbase,
commit: output.commitment(),
proof: output.proof(),
}
}
}
impl From<&TxKernel> for CbKernelV5 {
fn from(kernel: &TxKernel) -> CbKernelV5 {
CbKernelV5 {
features: CbKernelFeatures::Coinbase,
excess: kernel.excess,
excess_sig: kernel.excess_sig,
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
enum CbOutputFeatures {
Coinbase,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
enum CbKernelFeatures {
Coinbase,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
struct CbOutputV5 {
features: CbOutputFeatures,
#[serde(serialize_with = "secp_ser::as_hex")]
commit: Commitment,
#[serde(serialize_with = "secp_ser::as_hex")]
proof: RangeProof,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct CbKernelV5 {
features: CbKernelFeatures,
#[serde(serialize_with = "secp_ser::as_hex")]
excess: Commitment,
#[serde(with = "secp_ser::sig_serde")]
excess_sig: secp::Signature,
}

View file

@ -0,0 +1,524 @@
// Copyright 2023 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Wraps a V5 Slate into a V5 Binary slate
use crate::grin_core::core::transaction::OutputFeatures;
use crate::grin_core::ser as grin_ser;
use crate::grin_core::ser::{Readable, Reader, Writeable, Writer};
use crate::grin_keychain::BlindingFactor;
use crate::grin_util::secp::key::PublicKey;
use crate::grin_util::secp::pedersen::{Commitment, RangeProof};
use crate::grin_util::secp::Signature;
use chrono::{DateTime, NaiveDateTime, Utc};
use ed25519_dalek::PublicKey as DalekPublicKey;
use ed25519_dalek::Signature as DalekSignature;
use std::convert::{TryFrom, TryInto};
use std::f64::consts::E;
use crate::slate_versions::v5::{
CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, PaymentMemoV5, SlateStateV5,
SlateV5, VersionCompatInfoV5,
};
use crate::slate_versions::v4_bin::{SlateOptFields, UuidWrap};
impl Writeable for SlateStateV5 {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
let b = match self {
SlateStateV5::Unknown => 0,
SlateStateV5::Standard1 => 1,
SlateStateV5::Standard2 => 2,
SlateStateV5::Standard3 => 3,
SlateStateV5::Invoice1 => 4,
SlateStateV5::Invoice2 => 5,
SlateStateV5::Invoice3 => 6,
};
writer.write_u8(b)
}
}
impl Readable for SlateStateV5 {
fn read<R: Reader>(reader: &mut R) -> Result<SlateStateV5, grin_ser::Error> {
let b = reader.read_u8()?;
let sta = match b {
0 => SlateStateV5::Unknown,
1 => SlateStateV5::Standard1,
2 => SlateStateV5::Standard2,
3 => SlateStateV5::Standard3,
4 => SlateStateV5::Invoice1,
5 => SlateStateV5::Invoice2,
6 => SlateStateV5::Invoice3,
_ => SlateStateV5::Unknown,
};
Ok(sta)
}
}
struct SigsWrap(Vec<ParticipantDataV5>);
struct SigsWrapRef<'a>(&'a Vec<ParticipantDataV5>);
impl<'a> Writeable for SigsWrapRef<'a> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_u8(self.0.len() as u8)?;
for s in self.0.iter() {
//0 means part sig is not yet included
//1 means part sig included
if s.part.is_some() {
writer.write_u8(1)?;
} else {
writer.write_u8(0)?;
}
s.xs.write(writer)?;
s.nonce.write(writer)?;
if let Some(s) = s.part {
s.write(writer)?;
}
}
Ok(())
}
}
impl Readable for SigsWrap {
fn read<R: Reader>(reader: &mut R) -> Result<SigsWrap, grin_ser::Error> {
let sigs_len = reader.read_u8()?;
let sigs = {
let mut ret = vec![];
for _ in 0..sigs_len as usize {
let has_partial = reader.read_u8()?;
let c = ParticipantDataV5 {
xs: PublicKey::read(reader)?,
nonce: PublicKey::read(reader)?,
part: match has_partial {
1 => Some(Signature::read(reader)?),
0 | _ => None,
},
};
ret.push(c);
}
ret
};
Ok(SigsWrap(sigs))
}
}
/// Serialization of optional structs
struct SlateOptStructsRef<'a> {
/// coms, default none
pub coms: &'a Option<Vec<CommitsV5>>,
///// proof, default none
pub proof: &'a Option<PaymentInfoV5>,
}
/// Serialization of optional structs
struct SlateOptStructs {
/// coms, default none
pub coms: Option<Vec<CommitsV5>>,
/// proof, default none
pub proof: Option<PaymentInfoV5>,
}
impl<'a> Writeable for SlateOptStructsRef<'a> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
// Status byte, bits determing which optional structs are serialized
// 0 0 0 0 0 0 1 1
// p c
let mut status = 0u8;
if self.coms.is_some() {
status |= 0x01
};
if self.proof.is_some() {
status |= 0x02
};
writer.write_u8(status)?;
if let Some(c) = self.coms {
ComsWrapRef(&c).write(writer)?;
}
if let Some(p) = self.proof {
ProofWrapRef(&p).write(writer)?;
}
Ok(())
}
}
impl Readable for SlateOptStructs {
fn read<R: Reader>(reader: &mut R) -> Result<SlateOptStructs, grin_ser::Error> {
let status = reader.read_u8()?;
let coms = if status & 0x01 > 0 {
Some(ComsWrap::read(reader)?.0)
} else {
None
};
let proof = if status & 0x02 > 0 {
Some(ProofWrap::read(reader)?.0)
} else {
None
};
Ok(SlateOptStructs { coms, proof })
}
}
struct ComsWrap(Vec<CommitsV5>);
struct ComsWrapRef<'a>(&'a Vec<CommitsV5>);
impl<'a> Writeable for ComsWrapRef<'a> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_u16(self.0.len() as u16)?;
for o in self.0.iter() {
//0 means input
//1 means output with proof
if o.p.is_some() {
writer.write_u8(1)?;
} else {
writer.write_u8(0)?;
}
OutputFeatures::from(o.f).write(writer)?;
o.c.write(writer)?;
if let Some(p) = o.p {
p.write(writer)?;
}
}
Ok(())
}
}
impl Readable for ComsWrap {
fn read<R: Reader>(reader: &mut R) -> Result<ComsWrap, grin_ser::Error> {
let coms_len = reader.read_u16()?;
let coms = {
let mut ret = vec![];
for _ in 0..coms_len as usize {
let is_output = reader.read_u8()?;
let c = CommitsV5 {
f: OutputFeatures::read(reader)?.into(),
c: Commitment::read(reader)?,
p: match is_output {
1 => Some(RangeProof::read(reader)?),
0 | _ => None,
},
};
ret.push(c);
}
ret
};
Ok(ComsWrap(coms))
}
}
struct ProofWrap(PaymentInfoV5);
struct ProofWrapRef<'a>(&'a PaymentInfoV5);
impl<'a> Writeable for ProofWrapRef<'a> {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
writer.write_fixed_bytes(self.0.saddr.to_bytes())?;
writer.write_fixed_bytes(self.0.raddr.to_bytes())?;
writer.write_i64(self.0.ts.timestamp())?;
match self.0.psig {
Some(s) => {
writer.write_u8(1)?;
writer.write_fixed_bytes(&s.to_bytes().to_vec())?;
}
None => writer.write_u8(0)?,
}
match &self.0.memo {
Some(s) => {
writer.write_u8(1)?;
writer.write_u8(s.memo_type)?;
writer.write_fixed_bytes(&s.memo)?;
}
None => writer.write_u8(0)?,
}
Ok(())
}
}
impl Readable for ProofWrap {
fn read<R: Reader>(reader: &mut R) -> Result<ProofWrap, grin_ser::Error> {
let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
let ts_raw: i64 = match reader.read_i64() {
Ok(v) => v,
Err(_) => return Err(grin_ser::Error::CorruptedData),
};
let ts_opt = match NaiveDateTime::from_timestamp_opt(ts_raw, 0) {
Some(o) => o,
None => return Err(grin_ser::Error::CorruptedData),
};
let ts = DateTime::<Utc>::from_utc(ts_opt, Utc);
let psig = match reader.read_u8()? {
0 => None,
1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()),
};
let memo = match reader.read_u8()? {
0 => None,
1 | _ => Some(PaymentMemoV5 {
memo_type: reader.read_u8().unwrap(),
memo: reader.read_fixed_bytes(32)?.try_into().unwrap_or_default(),
}),
};
Ok(ProofWrap(PaymentInfoV5 {
saddr,
raddr,
ts,
psig,
memo,
}))
}
}
#[derive(Debug, Clone)]
pub struct SlateV5Bin(pub SlateV5);
impl From<SlateV5> for SlateV5Bin {
fn from(slate: SlateV5) -> SlateV5Bin {
SlateV5Bin(slate)
}
}
impl From<SlateV5Bin> for SlateV5 {
fn from(slate: SlateV5Bin) -> SlateV5 {
slate.0
}
}
impl serde::Serialize for SlateV5Bin {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut vec = vec![];
grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self)
.map_err(|err| serde::ser::Error::custom(err.to_string()))?;
serializer.serialize_bytes(&vec)
}
}
impl<'de> serde::Deserialize<'de> for SlateV5Bin {
fn deserialize<D>(deserializer: D) -> Result<SlateV5Bin, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SlateV5BinVisitor;
impl<'de> serde::de::Visitor<'de> for SlateV5BinVisitor {
type Value = SlateV5Bin;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a serialised binary V5 slate")
}
fn visit_bytes<E>(self, value: &[u8]) -> Result<SlateV5Bin, E>
where
E: serde::de::Error,
{
let mut reader = std::io::Cursor::new(value.to_vec());
let s = grin_ser::deserialize(
&mut reader,
grin_ser::ProtocolVersion(4),
grin_ser::DeserializationMode::default(),
)
.map_err(|err| serde::de::Error::custom(err.to_string()))?;
Ok(s)
}
}
deserializer.deserialize_bytes(SlateV5BinVisitor)
}
}
impl Writeable for SlateV5Bin {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), grin_ser::Error> {
let v5 = &self.0;
writer.write_u16(v5.ver.version)?;
writer.write_u16(v5.ver.block_header_version)?;
(UuidWrap(v5.id)).write(writer)?;
v5.sta.write(writer)?;
v5.off.write(writer)?;
SlateOptFields {
num_parts: v5.num_parts,
amt: v5.amt,
fee: v5.fee,
feat: v5.feat,
ttl: v5.ttl,
}
.write(writer)?;
(SigsWrapRef(&v5.sigs)).write(writer)?;
SlateOptStructsRef {
coms: &v5.coms,
proof: &v5.proof,
}
.write(writer)?;
// Write lock height for height locked kernels
if v5.feat == 2 {
let lock_hgt = match &v5.feat_args {
Some(l) => l.lock_hgt,
None => 0,
};
writer.write_u64(lock_hgt)?;
}
Ok(())
}
}
impl Readable for SlateV5Bin {
fn read<R: Reader>(reader: &mut R) -> Result<SlateV5Bin, grin_ser::Error> {
let ver = VersionCompatInfoV5 {
version: reader.read_u16()?,
block_header_version: reader.read_u16()?,
};
let id = UuidWrap::read(reader)?.0;
let sta = SlateStateV5::read(reader)?;
let off = BlindingFactor::read(reader)?;
let opts = SlateOptFields::read(reader)?;
let sigs = SigsWrap::read(reader)?.0;
let opt_structs = SlateOptStructs::read(reader)?;
let feat_args = if opts.feat == 2 {
Some(KernelFeaturesArgsV5 {
lock_hgt: reader.read_u64()?,
})
} else {
None
};
Ok(SlateV5Bin(SlateV5 {
ver,
id,
sta,
off,
num_parts: opts.num_parts,
amt: opts.amt,
fee: opts.fee,
feat: opts.feat,
ttl: opts.ttl,
sigs,
coms: opt_structs.coms,
proof: opt_structs.proof,
feat_args,
}))
}
}
#[test]
fn slate_v5_serialize_deserialize() {
use crate::grin_util::from_hex;
use crate::grin_util::secp::key::PublicKey;
use crate::Slate;
use grin_core::global::{set_local_chain_type, ChainTypes};
use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType};
set_local_chain_type(ChainTypes::Mainnet);
let slate = Slate::blank(1, false);
let mut v5 = SlateV5::from(slate);
use chrono::prelude::*;
let keychain = ExtKeychain::from_random_seed(true).unwrap();
let switch = SwitchCommitmentType::Regular;
// add some sig data
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0);
let skey1 = keychain.derive_key(0, &id1, switch).unwrap();
let skey2 = keychain.derive_key(0, &id2, switch).unwrap();
let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap();
let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap();
let part = ParticipantDataV5 {
xs,
nonce,
part: None,
};
let part2 = ParticipantDataV5 {
xs,
nonce,
part: Some(Signature::from_raw_data(&[11; 64]).unwrap()),
};
v5.sigs.push(part.clone());
v5.sigs.push(part2);
v5.sigs.push(part);
// add some random commit data
let com1 = CommitsV5 {
f: OutputFeatures::Plain.into(),
c: Commitment::from_vec([3u8; 1].to_vec()),
p: None,
};
let com2 = CommitsV5 {
f: OutputFeatures::Plain.into(),
c: Commitment::from_vec([4u8; 1].to_vec()),
p: Some(RangeProof::zero()),
};
let mut coms = vec![];
coms.push(com1.clone());
coms.push(com1.clone());
coms.push(com1.clone());
coms.push(com2);
v5.coms = Some(coms);
v5.amt = 234324899824;
v5.feat = 1;
v5.num_parts = 2;
v5.feat_args = Some(KernelFeaturesArgsV5 { lock_hgt: 23092039 });
let v4_1 = v5.clone();
let v4_1_copy = v5.clone();
let v4_bin = SlateV5Bin(v5);
let mut vec = Vec::new();
let _ = grin_ser::serialize_default(&mut vec, &v4_bin).expect("serialization failed");
let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap();
let v4_2 = b4_bin_2.0.clone();
assert_eq!(v4_1.ver, v4_2.ver);
assert_eq!(v4_1.id, v4_2.id);
assert_eq!(v4_1.amt, v4_2.amt);
assert_eq!(v4_1.fee, v4_2.fee);
let v4_2_coms = v4_2.coms.as_ref().unwrap().clone();
for (i, c) in v4_1.coms.unwrap().iter().enumerate() {
assert_eq!(c.f, v4_2_coms[i].f);
assert_eq!(c.c, v4_2_coms[i].c);
assert_eq!(c.p, v4_2_coms[i].p);
}
assert_eq!(v4_1.sigs, v4_2.sigs);
assert_eq!(v4_1.proof, v4_2.proof);
// Include Payment proof, remove coms to mix it up a bit
let mut v5 = v4_1_copy;
let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb";
let b = from_hex(raw_pubkey_str).unwrap();
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
// Need to remove milliseconds component for comparison. Won't be serialized
let ts = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
let ts = DateTime::<Utc>::from_utc(ts, Utc);
let pm = PaymentMemoV5 {
memo_type: 0,
memo: [9; 32],
};
v5.proof = Some(PaymentInfoV5 {
raddr: d_pkey.clone(),
saddr: d_pkey.clone(),
ts: ts.clone(),
psig: None,
memo: Some(pm),
});
v5.coms = None;
let v5_1 = v5.clone();
let v5_bin = SlateV5Bin(v5);
let mut vec = Vec::new();
let _ = grin_ser::serialize_default(&mut vec, &v5_bin).expect("serialization failed");
let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap();
let v5_2 = b4_bin_2.0.clone();
assert_eq!(v5_1.ver, v5_2.ver);
assert_eq!(v5_1.id, v5_2.id);
assert_eq!(v5_1.amt, v5_2.amt);
assert_eq!(v5_1.fee, v5_2.fee);
assert!(v5_1.coms.is_none());
assert_eq!(v5_1.sigs, v5_2.sigs);
assert_eq!(v5_1.proof, v5_2.proof);
}

View file

@ -189,10 +189,10 @@ fn format_slatepack(slatepack: &str) -> Result<String, Error> {
// Returns the first four bytes of a double sha256 hash of some bytes
fn generate_check(payload: &[u8]) -> Result<Vec<u8>, Error> {
let mut first_hash = Sha256::new();
first_hash.input(payload);
first_hash.update(payload);
let mut second_hash = Sha256::new();
second_hash.input(first_hash.result());
let checksum = second_hash.result();
second_hash.update(first_hash.finalize());
let checksum = second_hash.finalize();
let check_bytes: Vec<u8> = checksum[0..4].to_vec();
Ok(check_bytes)
}

View file

@ -94,7 +94,7 @@ impl<'a> Slatepacker<'a> {
/// Create slatepack from slate and args
pub fn create_slatepack(&self, slate: &Slate) -> Result<Slatepack, Error> {
let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?;
let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?;
let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?;
let mut slatepack = Slatepack::default();
slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?;

View file

@ -190,8 +190,8 @@ impl Slatepack {
let mut b = [0u8; 32];
b.copy_from_slice(&dec_key.as_bytes()[0..32]);
let mut hasher = Sha512::new();
hasher.input(b);
let result = hasher.result();
hasher.update(b);
let result = hasher.finalize();
b.copy_from_slice(&result[0..32]);
let x_dec_secret = StaticSecret::from(b);

View file

@ -16,6 +16,7 @@
//! implementation
use crate::config::{TorConfig, WalletConfig};
use crate::contract::types::ContractSetupArgsAPI;
use crate::error::Error;
use crate::grin_core::core::hash::Hash;
use crate::grin_core::core::FeeFields;
@ -25,8 +26,9 @@ use crate::grin_core::{global, ser};
use crate::grin_keychain::{Identifier, Keychain};
use crate::grin_util::logger::LoggingConfig;
use crate::grin_util::secp::key::{PublicKey, SecretKey};
use crate::grin_util::secp::{self, pedersen, Secp256k1};
use crate::grin_util::secp::{self, pedersen, Secp256k1, Signature};
use crate::grin_util::{ToHex, ZeroingString};
use crate::slate::PaymentMemo;
use crate::slate_versions::ser as dalek_ser;
use crate::InitTxArgs;
use chrono::prelude::*;
@ -189,7 +191,15 @@ where
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error>;
/// Get an (Optional) tx log entry by uuid
fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
// TODO: I think this can be deleted
// fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
/// Get an (Optional) tx log entry by uuid
fn get_tx_log_entry(
&self,
parent_id: Identifier,
log_id: u32,
) -> Result<Option<TxLogEntry>, Error>;
/// Retrieves the private context associated with a given slate id
fn get_private_context(
@ -563,6 +573,13 @@ pub struct Context {
/// for invoice I2 Only, store the tx excess so we can
/// remove it from the slate on return
pub calculated_excess: Option<pedersen::Commitment>,
/// Arguments that define which outputs to pick for a contract
pub setup_args: Option<ContractSetupArgsAPI>,
/// TxLogEntry id (needed to avoid a linear scan)
// Services that keep a long history might need to search
// through a list when they need to update a txlogentry.
// This is why we keep the id in the context.
pub log_id: Option<u32>,
}
impl Context {
@ -612,6 +629,8 @@ impl Context {
payment_proof_derivation_index: None,
late_lock_args: None,
calculated_excess: None,
setup_args: None,
log_id: None,
}
}
}
@ -652,6 +671,11 @@ impl Context {
PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(),
)
}
/// Returns net_change for the contract
pub fn get_net_change(&self) -> i64 {
self.setup_args.as_ref().unwrap().net_change.unwrap()
}
}
impl ser::Writeable for Context {
@ -767,6 +791,10 @@ pub enum TxLogEntryType {
TxReceivedCancelled,
/// Sent transaction that was rolled back by user
TxSentCancelled,
/// Self spend, as per contracts and mwmixnet
TxSelfSpend,
/// Self Spend Cancelled (has to happen before sent to chain, flag rather than delete)
TxSelfSpendCancelled,
/// Received transaction that was reverted on-chain
TxReverted,
}
@ -780,6 +808,8 @@ impl fmt::Display for TxLogEntryType {
TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"),
TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"),
TxLogEntryType::TxReverted => write!(f, "Received Tx\n- Reverted"),
TxLogEntryType::TxSelfSpend => write!(f, "Self Spend"),
TxLogEntryType::TxSelfSpendCancelled => write!(f, "Self Spend\n- Cancelled"),
}
}
}
@ -911,6 +941,23 @@ pub struct StoredProofInfo {
/// sender signature
#[serde(with = "dalek_ser::option_dalek_sig_serde")]
pub sender_signature: Option<DalekSignature>,
// Fields beyond here are specific to early payment proofs,
// invoice and sender nonce
/// Assumed to be 0x00 (Legacy) if missing
pub proof_type: Option<u8>,
/// receiver's public nonce from signing
pub receiver_public_nonce: Option<PublicKey>,
/// receiver's public excess from signing
pub receiver_public_excess: Option<PublicKey>,
/// Timestamp provided by recipient when signing
pub timestamp: Option<DateTime<Utc>>,
/// Optional payment memo
pub memo: Option<PaymentMemo>,
/// recipient promise signature
#[serde(with = "dalek_ser::option_dalek_sig_serde")]
pub promise_signature: Option<DalekSignature>,
/// Original Sender partial key
pub sender_part_sig: Option<Signature>,
}
impl ser::Writeable for StoredProofInfo {

View file

@ -446,3 +446,149 @@ subcommands:
- input:
help: Filename of a proof file
index: 1
- contract:
subcommands:
- new:
about: Create a new contract with initial setup
args:
- encrypt-for:
help: The counter party grin address
short: e
long: encrypt-for
takes_value: true
# index: 1
- receive:
help: How much you want to receive
short: r
long: receive
takes_value: true
- send:
help: How much you want to send
short: s
long: send
takes_value: true
- num-participants:
help: How many participants are involved? (can be 1 or 2)
short: n
long: num-participants
takes_value: true
default_value: "2"
- as-json:
help: Show result as JSON
short: j
long: as-json
takes_value: false
- no-payjoin:
help: Don't make it a payjoin (if receiver)
long: no-payjoin
takes_value: false
- add-outputs:
help: Defines whether we should pick inputs/outputs in the first step.
short: a
long: add-outputs
takes_value: false
- use-inputs:
help: Which inputs you want to use (provide comma separated commitments)
short: i
long: use-inputs
takes_value: true
- make-outputs:
help: Which outputs should we create? (provide comma separated amounts)
short: o
long: make-outputs
takes_value: true
# - setup:
# about: Perform a key setup on a contract
# args:
# - encrypt-for:
# help: The counter party grin address
# short: e
# long: encrypt-for
# takes_value: true
# # index: 1
# - receive:
# help: How much you want to receive
# short: r
# long: receive
# takes_value: true
# - send:
# help: How much you want to send
# short: s
# long: send
# takes_value: true
# - as-json:
# help: Show result as JSON
# short: j
# long: as-json
# takes_value: false
# - no-payjoin:
# help: Don't make it a payjoin (if receiver)
# long: no-payjoin
# takes_value: false
# - add-outputs:
# help: Defines whether we should pick inputs/outputs in the setup step.
# short: a
# long: add-outputs
# takes_value: false
# - use-inputs:
# help: Which inputs you want to use (provide comma separated commitments)
# short: i
# long: use-inputs
# takes_value: true
# - make-outputs:
# help: Which outputs should we create? (provide comma separated amounts)
# short: o
# long: make-outputs
# takes_value: true
- sign:
about: Sign a contract
args:
- encrypt-for:
help: The counter party grin address
short: e
long: encrypt-for
takes_value: true
# index: 1
- receive:
help: How much you want to receive
short: r
long: receive
takes_value: true
- send:
help: How much you want to send
short: s
long: send
takes_value: true
- as-json:
help: Show result as JSON
short: j
long: as-json
takes_value: false
- no-payjoin:
help: Don't make it a payjoin (if receiver)
long: no-payjoin
takes_value: false
- use-inputs:
help: Which inputs you want to use (provide comma separated commitments)
short: i
long: use-inputs
takes_value: true
- make-outputs:
help: Which outputs should we create? (provide comma separated amounts)
short: o
long: make-outputs
takes_value: true
- no-broadcast:
help: Don't broadcast the transaction if it was finalized
long: no-broadcast
takes_value: false
- view:
about: View a contract
- revoke:
about: Attempt to revoke a contract
args:
- tx-id:
help: Id of a transaction we want to cancel
short: i
long: tx-id
takes_value: true

View file

@ -39,13 +39,10 @@ fn main() {
}
// build and versioning information
let mut opts = built::Options::default();
opts.set_dependencies(true);
let out_dir_path = format!("{}{}", env::var("OUT_DIR").unwrap(), "/built.rs");
// don't fail the build if something's missing, may just be cargo release
let _ = built::write_built_file_with_opts(
&opts,
Path::new(env!("CARGO_MANIFEST_DIR")),
Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
Path::new(&out_dir_path),
);
}

View file

@ -12,13 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/// Argument parsing and error handling for wallet commands
use self::core::consensus;
use crate::api::TLSConfig;
use crate::cli::command_loop;
use crate::config::GRIN_WALLET_DIR;
use crate::util::file::get_first_line;
use crate::util::secp::key::SecretKey;
use crate::util::{Mutex, ZeroingString};
/// Argument parsing and error handling for wallet commands
use clap::ArgMatches;
use grin_core as core;
use grin_core::core::amount_to_hr_string;
@ -965,6 +966,148 @@ pub fn parse_verify_proof_args(args: &ArgMatches) -> Result<command::ProofVerify
})
}
// TODO: parse args
pub fn parse_contract_new_args(
args: &ArgMatches,
account: &String,
) -> Result<command::ContractNewArgs, ParseError> {
let counterparty_addr = args.value_of("encrypt-for").unwrap();
// TODO: Make sure the values are in some expected bounds.
// TODO: How to deal with decimals and precision? we probably want users to express the value in Grin.
// Parse receive and send params and convert them to nano grin
let receive = match args.value_of("receive") {
Some(g) => Some((g.parse::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
let send = match args.value_of("send") {
Some(g) => Some((g.parse::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
if receive.is_some() && send.is_some() {
return Err(ParseError::ArgumentError(String::from(
"You can only specify receive or send, not both.",
)));
};
// TODO: verify this is correct e.g. which values are passed here by default etc.
let src_acct_name = Some(String::from(account));
let add_outputs = args.is_present("add-outputs");
let as_json = args.is_present("as-json");
let no_payjoin = args.is_present("no-payjoin");
let use_inputs = match args.value_of("use-inputs") {
Some(v) => {
if no_payjoin {
panic!("Can't use --no-payjoin with --use-inputs.");
}
Some(String::from(v))
}
None => {
if no_payjoin {
None
} else {
// Some("any") means pick 1 random input to contribute (payjoin)
Some(String::from("any"))
}
}
};
let make_outputs = match args.value_of("make-outputs") {
Some(v) => Some(String::from(v)),
None => None,
};
let num_participants = args
.value_of("num-participants")
.unwrap()
.parse::<u8>()
.unwrap();
Ok(command::ContractNewArgs {
counterparty_addr: String::from(counterparty_addr),
receive: receive,
send: send,
src_acct_name: src_acct_name,
num_participants: num_participants,
as_json: as_json,
add_outputs: add_outputs,
use_inputs: use_inputs,
make_outputs: make_outputs,
// TODO: Future features below
fee_rate: None,
outfile: None,
})
}
// TODO: parse args
pub fn parse_contract_setup_args(
args: &ArgMatches,
) -> Result<command::ContractSetupArgs, ParseError> {
// TODO: Make sure the values are in some expected bounds.
// TODO: How to deal with decimals and precision? we probably want users to express the value in Grin.
let counterparty_addr = match args.value_of("encrypt-for") {
Some(v) => Some(String::from(v)),
None => None,
};
// Parse receive and send params and convert them to nano grin
let receive = match args.value_of("receive") {
Some(g) => Some((g.parse::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
let send = match args.value_of("send") {
Some(g) => Some((g.parse::<f64>().unwrap() * consensus::GRIN_BASE as f64) as u64),
None => None,
};
if receive.is_some() && send.is_some() {
return Err(ParseError::ArgumentError(String::from(
"You can only specify receive or send, not both.",
)));
};
let as_json = args.is_present("as-json");
let no_payjoin = args.is_present("no-payjoin");
let use_inputs = match args.value_of("use-inputs") {
Some(v) => {
if no_payjoin {
panic!("Can't use --no-payjoin with --use-inputs.");
}
Some(String::from(v))
}
None => {
if no_payjoin {
None
} else {
// Some("any") means pick 1 random input to contribute (payjoin)
Some(String::from("any"))
}
}
};
let make_outputs = match args.value_of("make-outputs") {
Some(v) => Some(String::from(v)),
None => None,
};
// TODO: should we catch if the person calls "--receive=5" when it should be "--send=5"?
// Perhaps we could detect this from the slate state e.g. S1 -> receive, I1 -> send?
Ok(command::ContractSetupArgs {
counterparty_addr: counterparty_addr,
receive: receive,
send: send,
as_json: as_json,
add_outputs: false,
use_inputs: use_inputs,
make_outputs: make_outputs,
// TODO: Future features below
fee_rate: None,
outfile: None,
})
}
pub fn parse_contract_revoke_args(
args: &ArgMatches,
) -> Result<command::ContractRevokeArgs, ParseError> {
let tx_id = args.value_of("tx-id").unwrap().parse::<u32>().unwrap();
Ok(command::ContractRevokeArgs { tx_id: tx_id })
}
pub fn wallet_command<C, F>(
wallet_args: &ArgMatches,
mut wallet_config: WalletConfig,
@ -1295,6 +1438,31 @@ where
// for CLI mode only, should be handled externally
Ok(())
}
("contract", Some(args)) => match args.subcommand() {
("new", Some(new_args)) => {
let account = &global_wallet_args.account;
let a = arg_parse!(parse_contract_new_args(&new_args, account));
command::contract_new(owner_api, km, a)
}
// ("setup", Some(setup_args)) => {
// let a = arg_parse!(parse_contract_setup_args(&setup_args));
// command::contract_setup(owner_api, km, a)
// }
("sign", Some(sign_args)) => {
// Sign command takes setup_args so we use the same parser
let setup_args = arg_parse!(parse_contract_setup_args(&sign_args));
let broadcast_tx = !sign_args.is_present("no-broadcast");
command::contract_sign(owner_api, km, setup_args, broadcast_tx)
}
("view", Some(view_args)) => {
Err(Error::ArgumentError(String::from("Not implemented")).into())
}
("revoke", Some(revoke_args)) => {
let a = arg_parse!(parse_contract_revoke_args(&revoke_args));
command::contract_revoke(owner_api, km, a)
}
_ => Err(Error::ArgumentError(String::from("Unknown contract subcommand.")).into()),
},
_ => {
let msg = format!("Unknown wallet command, use 'grin-wallet help' for details");
return Err(Error::ArgumentError(msg));

View file

@ -1,6 +1,6 @@
[package]
name = "grin_wallet_util"
version = "5.4.0-alpha.1"
version = "5.4.0-contracts.0"
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
description = "Util, for generic utilities and to re-export grin crates"
license = "Apache-2.0"
@ -21,7 +21,7 @@ thiserror = "1"
##### Grin Imports
# For Release
# grin_util = "5.4.0-alpha.1"
# grin_util = "5.4.0-contracts.0"
# For beta release