Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payments with balance #768

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 47 additions & 33 deletions chain-signatures/contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use near_sdk::{
PromiseError, PublicKey,
};
use primitives::{
CandidateInfo, Candidates, Participants, PkVotes, SignRequest, SignaturePromiseError,
Balances, CandidateInfo, Candidates, Participants, PkVotes, SignRequest, SignaturePromiseError,
SignatureRequest, SignatureResult, StorageKey, Votes, YieldIndex,
};
use std::collections::{BTreeMap, HashSet};
Expand All @@ -32,6 +32,7 @@ pub use state::{
InitializingContractState, ProtocolContractState, ResharingContractState, RunningContractState,
};

// TODO: How much gas we need for y/r sign call?
const GAS_FOR_SIGN_CALL: Gas = Gas::from_tgas(250);

// Register used to receive data id from `promise_await_data`.
Expand Down Expand Up @@ -62,6 +63,7 @@ impl Default for VersionedMpcContract {
pub struct MpcContract {
protocol_state: ProtocolContractState,
pending_requests: LookupMap<SignatureRequest, YieldIndex>,
balances: Balances,
request_counter: u32,
proposed_updates: ProposedUpdates,
config: Config,
Expand Down Expand Up @@ -102,6 +104,7 @@ impl MpcContract {
request_counter: 0,
proposed_updates: ProposedUpdates::default(),
config: config.unwrap_or_default(),
balances: Balances::default(),
}
}
}
Expand Down Expand Up @@ -132,16 +135,8 @@ impl VersionedMpcContract {
SignError::UnsupportedKeyVersion,
));
}
// Check deposit
let deposit = env::attached_deposit();
let required_deposit = self.signature_deposit();
if deposit.as_yoctonear() < required_deposit {
return Err(MpcContractError::SignError(SignError::InsufficientDeposit(
deposit.as_yoctonear(),
required_deposit,
)));
}
// Make sure sign call will not run out of gas doing recursive calls because the payload will never be removed
// TODO: make sure it is not possible to call payable function with LAK
// Make sure sign call will not run out of gas
if env::prepaid_gas() < GAS_FOR_SIGN_CALL {
return Err(MpcContractError::SignError(SignError::InsufficientGas(
env::prepaid_gas(),
Expand All @@ -157,7 +152,8 @@ impl VersionedMpcContract {
}
}
let predecessor = env::predecessor_account_id();
let request = SignatureRequest::new(payload, &predecessor, &path);
let signature_fee = NearToken::from_yoctonear(self.signature_fee());
let request = SignatureRequest::new(payload, predecessor.clone(), &path, signature_fee);
if !self.request_already_exists(&request) {
log!(
"sign: predecessor={predecessor}, payload={payload:?}, path={path:?}, key_version={key_version}",
Expand Down Expand Up @@ -209,6 +205,33 @@ impl VersionedMpcContract {
pub const fn latest_key_version(&self) -> u32 {
0
}

#[payable]
pub fn top_up(&mut self, account_id: Option<AccountId>) {
let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
let deposit = env::attached_deposit();
log!(
"add_deposit: account_id={:?}, deposit={}",
account_id,
deposit
);
match self {
Self::V0(mpc_contract) => {
mpc_contract.balances.top_up(&account_id, deposit);
}
}
}

pub fn get_balance(&self, account_id: Option<AccountId>) -> Option<NearToken> {
let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
match self {
Self::V0(mpc_contract) => mpc_contract.balances.get(&account_id),
}
}

pub fn signature_fee(&self) -> u128 {
self.signature_deposit().as_yoctonear()
}
}

// Node API
Expand Down Expand Up @@ -254,6 +277,11 @@ impl VersionedMpcContract {

match self {
Self::V0(mpc_contract) => {
// withdraw the signature fee
mpc_contract
.balances
.withdraw(&request.account_id, request.signature_fee);
Copy link

Choose a reason for hiding this comment

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

@volovyks the recommended pattern for handling balances is:

  • withdraw on request
  • refund on error

Otherwise, you could get a lot of requests, which you approve since the user has the balance, and then discover during the response that the user actually has no money to pay for them all

So in this case, we would want to use balances.withdraw on sign, and return_signature would refund the user if they never got the signature because of a response timeout

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That is true. We can fail in multiple places, so this design seemed simpler and more robust.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will come back to it if we will choose this direction.

// respond to the request
if let Some(YieldIndex { data_id }) =
mpc_contract.pending_requests.get(&request)
{
Expand Down Expand Up @@ -619,6 +647,7 @@ impl VersionedMpcContract {
threshold: usize,
public_key: PublicKey,
config: Option<Config>,
balances: Balances,
) -> Result<Self, MpcContractError> {
log!(
"init_running: signer={}, epoch={}, participants={}, threshold={}, public_key={:?}",
Expand All @@ -643,6 +672,7 @@ impl VersionedMpcContract {
join_votes: Votes::new(),
leave_votes: Votes::new(),
}),
balances,
pending_requests: LookupMap::new(StorageKey::PendingRequests),
request_counter: 0,
proposed_updates: ProposedUpdates::default(),
Expand Down Expand Up @@ -762,22 +792,6 @@ impl VersionedMpcContract {
}
}

#[private]
#[init(ignore_state)]
pub fn clean(keys: Vec<near_sdk::json_types::Base64VecU8>) -> Self {
log!("clean: keys={:?}", keys);
for key in keys.iter() {
env::storage_remove(&key.0);
}
Self::V0(MpcContract {
protocol_state: ProtocolContractState::NotInitialized,
pending_requests: LookupMap::new(StorageKey::PendingRequests),
request_counter: 0,
proposed_updates: ProposedUpdates::default(),
config: Config::default(),
})
}

fn mutable_state(&mut self) -> &mut ProtocolContractState {
match self {
Self::V0(ref mut mpc_contract) => &mut mpc_contract.protocol_state,
Expand All @@ -790,17 +804,17 @@ impl VersionedMpcContract {
}
}

fn signature_deposit(&self) -> u128 {
fn signature_deposit(&self) -> NearToken {
const CHEAP_REQUESTS: u32 = 3;
let pending_requests = match self {
Self::V0(mpc_contract) => mpc_contract.request_counter,
};
match pending_requests {
0..=CHEAP_REQUESTS => 1,
_ => {
0..=CHEAP_REQUESTS => NearToken::from_yoctonear(1),
_ => NearToken::from_yoctonear(
(pending_requests - CHEAP_REQUESTS) as u128
* NearToken::from_millinear(50).as_yoctonear()
}
* NearToken::from_millinear(50).as_yoctonear(),
),
}
}

Expand Down
56 changes: 53 additions & 3 deletions chain-signatures/contract/src/primitives.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crypto_shared::{derive_epsilon, SerializableScalar};
use k256::Scalar;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LookupMap;
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{AccountId, BorshStorageKey, CryptoHash, PublicKey};
use near_sdk::{AccountId, BorshStorageKey, CryptoHash, NearToken, PublicKey};
use std::collections::{BTreeMap, HashMap, HashSet};

pub mod hpke {
Expand All @@ -14,6 +15,7 @@ pub mod hpke {
pub enum StorageKey {
PendingRequests,
ProposedUpdatesEntries,
Balances,
}

/// The index into calling the YieldResume feature of NEAR. This will allow to resume
Expand All @@ -29,18 +31,27 @@ pub struct YieldIndex {
pub struct SignatureRequest {
pub epsilon: SerializableScalar,
pub payload_hash: SerializableScalar,
pub account_id: AccountId,
pub signature_fee: NearToken,
}

impl SignatureRequest {
pub fn new(payload_hash: Scalar, predecessor_id: &AccountId, path: &str) -> Self {
let epsilon = derive_epsilon(predecessor_id, path);
pub fn new(
payload_hash: Scalar,
predecessor_id: AccountId,
path: &str,
signature_fee: NearToken,
) -> Self {
let epsilon = derive_epsilon(&predecessor_id, path);
let epsilon = SerializableScalar { scalar: epsilon };
let payload_hash = SerializableScalar {
scalar: payload_hash,
};
SignatureRequest {
epsilon,
payload_hash,
account_id: predecessor_id,
signature_fee,
}
}
}
Expand Down Expand Up @@ -278,3 +289,42 @@ pub enum SignatureResult<T, E> {
pub enum SignaturePromiseError {
Failed,
}

#[derive(Debug, BorshSerialize, BorshDeserialize)]
pub struct Balances {
entries: LookupMap<AccountId, NearToken>,
}

impl Default for Balances {
fn default() -> Self {
Self {
entries: LookupMap::new(StorageKey::Balances),
}
}
}

impl Balances {
pub fn get(&self, account_id: &AccountId) -> Option<NearToken> {
self.entries.get(account_id)
}

pub fn top_up(&mut self, account_id: &AccountId, amount: NearToken) {
let balance = self.entries.get(account_id).unwrap_or_default();
self.entries.insert(
account_id,
&NearToken::from_yoctonear(&balance.as_yoctonear() + &amount.as_yoctonear()),
);
}

pub fn withdraw(&mut self, account_id: &AccountId, amount: NearToken) -> bool {
let balance = self.entries.get(account_id).unwrap_or_default();
if amount > balance {
return false;
}
self.entries.insert(
account_id,
&NearToken::from_yoctonear(&balance.as_yoctonear() - &amount.as_yoctonear()),
);
true
}
}
Loading