Account type with state

comments

fixups!

fixups!

fixups for a real Result<> from get_balance()

on 2nd thought, be more rigorous

Merge branch 'rob-solana-accounts_with_state' into accounts_with_state

update

review comments

comments

get rid of option
This commit is contained in:
Anatoly Yakovenko 2018-08-10 21:13:34 -07:00 committed by Rob Walker
parent fc0d7f5982
commit a284030ecc
10 changed files with 323 additions and 192 deletions

View File

@ -18,13 +18,13 @@ fi
garbage_address=vS3ngn1TfQmpsW1Z4NkLuqNAQFF3dYQw8UZ6TCx9bmq
check_balance_output() {
declare expected_output="$1"
exec 42>&1
output=$($wallet balance | tee >(cat - >&42))
if [[ ! "$output" =~ $expected_output ]]; then
echo "Balance is incorrect. Expected: $expected_output"
exit 1
fi
declare expected_output="$1"
exec 42>&1
output=$($wallet balance | tee >(cat - >&42))
if [[ ! "$output" =~ $expected_output ]]; then
echo "Balance is incorrect. Expected: $expected_output"
exit 1
fi
}
pay_and_confirm() {
@ -35,7 +35,7 @@ pay_and_confirm() {
$wallet reset
$wallet address
check_balance_output "Your balance is: 0"
check_balance_output "No account found"
$wallet airdrop --tokens 60
check_balance_output "Your balance is: 60"
$wallet airdrop --tokens 40

View File

@ -4,7 +4,7 @@ The goal of this RFC is to define a set of constraints for APIs and runtime such
## Version
version 0.1
version 0.2
## Toolchain Stack
@ -37,154 +37,175 @@ version 0.1
In Figure 1 an untrusted client, creates a program in the front-end language of her choice, (like C/C++/Rust/Lua), and compiles it with LLVM to a position independent shared object ELF, targeting BPF bytecode. Solana will safely load and execute the ELF.
## Bytecode
Our bytecode is based on Berkley Packet Filter. The requirements for BPF overlap almost exactly with the requirements we have:
1. Deterministic amount of time to execute the code
2. Bytecode that is portable between machine instruction sets
3. Verified memory accesses
4. Fast to load the object, verify the bytecode and JIT to local machine instruction set
For 1, that means that loops are unrolled, and for any jumps back we can guard them with a check against the number of instruction that have been executed at this point. If the limit is reached, the program yields its execution. This involves saving the stack and current instruction index.
For 2, the BPF bytecode already easily maps to x8664, arm64 and other instruction sets. 
For 3, every load and store that is relative can be checked to be within the expected memory that is passed into the ELF. Dynamic load and stores can do a runtime check against available memory, these will be slow and should be avoided.
For 4, Fully linked PIC ELF with just a single RX segment. Effectively we are linking a shared object with `-fpic -target bpf` and with a linker script to collect everything into a single RX segment. Writable globals are not supported.
### Address Checks
The interface to the module takes a `&mut Vec<Vec<u8>>` in rust, or a `int sz, void* data[sz], int szs[sz]` in `C`. Given the module's bytecode, for each method, we need to analyze the bounds on load and stores into each buffer the module uses. This check needs to be done `on chain`, and after those bounds are computed we can verify that the user supplied array of buffers will not cause a memory fault. For load and stores that we cannot analyze, we can replace with a `safe_load` and `safe_store` instruction that will check the table for access.
## Loader
The loader is our first smart contract. The job of this contract is to load the actual program with its own instance data. The loader will verify the bytecode and that the object implements the expected entry points.
Since there is only one RX segment, the context for the contract instance is passed into each entry point as well as the event data for that entry point.
A client will create a transaction to create a new loader instance:
`Solana_NewLoader(Loader Instance PubKey, proof of key ownership, space I need for my elf)`
A client will then do a bunch of transactions to load its elf into the loader instance they created:
`Loader_UploadElf(Loader Instance PubKey, proof of key ownership, pos start, pos end, data)`
At this point the client can create a new instance of the module with its own instance address:
`Loader_NewInstance(Loader Instance PubKey, proof of key ownership, Instance PubKey, proof of key ownership)`
Once the instance has been created, the client may need to upload more user data to solana to configure this instance:
`Instance_UploadModuleData(Instance PubKey, proof of key ownership, pos start, pos end, data)`
Now clients can `start` the instance:
`Instance_Start(Instance PubKey, proof of key ownership)`
## Runtime
Our goal with the runtime is to have a general purpose execution environment that is highly parallelizable and doesn't require dynamic resource management. We want to execute as many contracts as we can in parallel, and have them pass or fail without a destructive state change.
### State and Entry Point
State is addressed by an account which is at the moment simply the PubKey. Our goal is to eliminate dynamic memory allocation in the smart contract itself, so the contract is a function that takes a mapping of [(PubKey,State)] and returns [(PubKey, State')]. The output of keys is a subset of the input. Three basic kinds of state exist:
* Instance State
* Participant State
* Caller State
There isn't any difference in how each is implemented, but conceptually Participant State is memory that is allocated for each participant in the contract. Instance State is memory that is allocated for the contract itself, and Caller State is memory that the transactions caller has allocated.
The goal with the runtime is to have a general purpose execution environment that is highly parallelizeable and doesn't require dynamic resource management. The goal is to execute as many contracts as possible in parallel, and have them pass or fail without a destructive state change.
### Call
### State
State is addressed by an account which is at the moment simply the Pubkey. Our goal is to eliminate memory allocation from within the smart contract itself. Thus the client of the contract provides all the state that is necessary for the contract to execute in the transaction itself. The runtime interacts with the contract through a state transition function, which takes a mapping of [(Pubkey,State)] and returns [(Pubkey, State')]. The State is an opeque type to the runtime, a `Vec<u8>`, the contents of which the contract has full control over.
### Call Structure
```
void call(
const struct instance_data *data,
const uint8_t kind[], //instance|participant|caller|read|write
const uint8_t *keys[],
uint8_t *data[],
int num,
uint8_t dirty[], //dirty memory bits
uint8_t *userdata, //current transaction data
);
/// Call definition
/// Signed portion
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CallData {
/// Each Pubkey in this vector is mapped to a corresponding `Page` that is loaded for contract execution
/// In a simple pay transaction `key[0]` is the token owner's key and `key[1]` is the recipient's key.
pub keys: Vec<Pubkey>,
/// The Pubkeys that are required to have a proof. The proofs are a `Vec<Signature> which encoded along side this data structure
/// Each Signature signs the `required_proofs` vector as well as the `keys` vectors. The transaction is valid if and only if all
/// the required signatures are present and the public key vector is unchanged between signatures.
pub required_proofs: Vec<u8>,
/// PoH data
/// last PoH hash observed by the sender
pub last_id: Hash,
/// Program
/// The address of the program we want to call. ContractId is just a Pubkey that is the address of the loaded code that will execute this Call.
pub contract_id: ContractId,
/// OS scheduling fee
pub fee: i64,
/// struct version to prevent duplicate spends
/// Calls with a version <= Page.version are rejected
pub version: u64,
/// method to call in the contract
pub method: u8,
/// usedata in bytes
pub userdata: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Call {
/// Signatures and Keys
/// (signature, key index)
/// This vector contains a tuple of signatures, and the key index the signature is for
/// proofs[0] is always key[0]
pub proofs: Vec<Signature>,
pub data: CallData,
}
```
To call this operation, the transaction that is destined to the contract instance specifies what keyed state it should present to the `call` function. To allocate the state memory or a call context, the client has to first call a function on the contract with the designed address that will own the state.
At it's core, this is just a set of Pubkeys and Signatures with a bit of metadata. The contract Pubkey routes this transaction into that contracts entry point. `version` is used for dropping retransmitted requests.
At its core, this is a system call that requires cryptographic proof of ownership of memory regions instead of an OS that checks page tables for access rights.
* `Instance_AllocateContext(Instance PubKey, My PubKey, Proof of key ownership)`
Any transaction can then call `call` on the contract with a set of keys. It's up to the contract itself to manage ownership:
* `Instance_Call(Instance PubKey, [Context PubKeys], proofs of ownership, userdata...)`
Contracts should be able to read any state that is part of solana, but only write to state that the contract allocated.
#### Caller State
Caller `state` is memory allocated for the `call` that belongs to the public key that is issuing the `call`. This is the caller's context.
#### Instance State
Instance `state` is memory that belongs to this contract instance. We may also need module-wide `state` as well.
#### Participant State
Participant `state` is any other memory. In some cases it may make sense to have these allocated as part of the call by the caller.
### Reduce
Some operations on the contract will require iteration over all the keys. To make this parallelizable the iteration is broken up into reduce calls which are combined.
```
void reduce_m(
const struct instance_data *data,
const uint8_t *keys[],
const uint8_t *data[],
int num,
uint8_t *reduce_data,
);
void reduce_r(
const struct instance_data *data,
const uint8_t *reduce_data[],
int num,
uint8_t *reduce_data,
);
```
Contracts should be able to read any state that is part of runtime, but only write to state that the contract allocated.
### Execution
Transactions are batched and processed in parallel at each stage.
```
+-----------+ +--------------+ +-----------+ +---------------+
| sigverify |-+->| debit commit |---+->| execution |-+->| memory commit |
+-----------+ | +--------------+ | +-----------+ | +---------------+
| | |
| +---------------+ | | +--------------+
|->| memory verify |->+ +->| debit undo |
+---------------+ | +--------------+
|
| +---------------+
+->| credit commit |
+---------------+
Calls batched and processed in a pipeline
```
The `debit verify` stage is very similar to `memory verify`. Proof of key ownership is used to check if the callers key has some state allocated with the contract, then the memory is loaded and executed. After execution stage, the dirty pages are written back by the contract. Because know all the memory accesses during execution, we can batch transactions that do not interfere with each other. We can also apply the `debit undo` and `credit commit` stages of the transaction. `debit undo` is run in case of an exception during contract execution, only transfers may be reversed, fees are commited to solana.
+-----------+ +-------------+ +--------------+ +--------------------+
| sigverify |--->| lock memory |--->| validate fee |--->| allocate new pages |--->
+-----------+ +-------------+ +--------------+ +--------------------+
+------------+ +---------+ +--------------+ +-=------------+
--->| load pages |--->| execute |--->|unlock memory |--->| commit pages |
+------------+ +---------+ +--------------+ +--------------+
### GPU execution
```
A single contract can read and write to separate key pairs without interference. These separate calls to the same contract can execute on the same GPU thread over different memory using different SIMD lanes.
At the `execute` stage, the loaded pages have no data dependencies, so all the contracts can be executed in parallel.
## Memory Management
```
pub struct Page {
/// key that indexes this page
/// prove ownership of this key to spend from this Page
owner: Pubkey,
/// contract that owns this page
/// contract can write to the data that is in `memory` vector
contract: Pubkey,
/// balance that belongs to owner
balance: u64,
/// version of the structure, public for testing
version: u64,
/// hash of the page data
memhash: Hash,
/// The following could be in a separate structure
memory: Vec<u8>,
}
```
The guarantee that runtime enforces:
1. The contract code is the only code that will modify the contents of `memory`
2. Total balances on all the pages is equal before and after exectuion of a call
3. Balances of each of the pages not owned by the contract must be equal to or greater after the call than before the call.
## Entry Point
Exectuion of the contract involves maping the contract's public key to an entry point which takes a pointer to the transaction, and an array of loaded pages.
```
// Find the method
match (tx.contract, tx.method) {
// system interface
// everyone has the same reallocate
(_, 0) => system_0_realloc(&tx, &mut call_pages),
(_, 1) => system_1_assign(&tx, &mut call_pages),
// contract methods
(DEFAULT_CONTRACT, 128) => default_contract_128_move_funds(&tx, &mut call_pages),
(contract, method) => //...
```
The first 127 methods are reserved for the system interface, which implements allocation and assignment of memory. The rest, including the contract for moving funds are implemented by the contract itself.
## System Interface
```
/// SYSTEM interface, same for very contract, methods 0 to 127
/// method 0
/// reallocate
/// spend the funds from the call to the first recipient's
pub fn system_0_realloc(call: &Call, pages: &mut Vec<Page>) {
if call.contract == DEFAULT_CONTRACT {
let size: u64 = deserialize(&call.userdata).unwrap();
pages[0].memory.resize(size as usize, 0u8);
}
}
/// method 1
/// assign
/// assign the page to a contract
pub fn system_1_assign(call: &Call, pages: &mut Vec<Page>) {
let contract = deserialize(&call.userdata).unwrap();
if call.contract == DEFAULT_CONTRACT {
pages[0].contract = contract;
//zero out the memory in pages[0].memory
//Contracts need to own the state of that data otherwise a use could fabricate the state and
//manipulate the contract
pages[0].memory.clear();
}
}
```
The first method resizes the memory that is assosciated with the callers page. The second system call assignes the page to the contract. Both methods check if the current contract is 0, otherwise the method does nothing and the caller spent their fees.
This ensures that when memory is assigned to the contract the initial state of all the bytes is 0, and the contract itself is the only thing that can modify that state.
## Simplest contract
```
/// DEFAULT_CONTRACT interface
/// All contracts start with 128
/// method 128
/// move_funds
/// spend the funds from the call to the first recipient's
pub fn default_contract_128_move_funds(call: &Call, pages: &mut Vec<Page>) {
let amount: u64 = deserialize(&call.userdata).unwrap();
if pages[0].balance >= amount {
pages[0].balance -= amount;
pages[1].balance += amount;
}
}
```
This simply moves the amount from page[0], which is the callers page, to page[1], which is the recipient's page.
## Notes
1. There is no dynamic memory allocation.
2. Persistant Memory is allocated to a Key with ownership
2. Persistent Memory is allocated to a Key with ownership
3. Contracts can `call` to update key owned state
4. Contracts can `reduce` over the memory to aggregate state
5. `call` is just a *syscall* that does a cryptographic check of memory owndershp
4. `call` is just a *syscall* that does a cryptographic check of memory ownership
5. Kernel guarantees that when memory is assigned to the contract its state is 0
6. Kernel guarantees that contract is the only thing that can modify memory that its assigned to
7. Kernel guarantees that the contract can only spend tokens that are in pages that are assigned to it
8. Kernel guarantees the balances belonging to pages are balanced before and after the call

View File

@ -1,4 +1,4 @@
//! The `bank` module tracks client balances and the progress of smart
//! The `bank` module tracks client accounts and the progress of smart
//! contracts. It offers a high-level API that signs transactions
//! on behalf of the caller, and a low-level API for when they have
//! already been signed and verified.
@ -65,11 +65,20 @@ pub enum BankError {
}
pub type Result<T> = result::Result<T, BankError>;
/// An Account with userdata that is stored on chain
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Account {
/// tokens in the account
pub tokens: i64,
/// user data
/// A transaction can write to its userdata
pub userdata: Vec<u8>,
}
/// The state of all accounts and contracts after processing its entries.
pub struct Bank {
/// A map of account public keys to the balance in that account.
balances: RwLock<HashMap<Pubkey, i64>>,
accounts: RwLock<HashMap<Pubkey, Account>>,
/// A map of smart contract transaction signatures to what remains of its payment
/// plan. Each transaction that targets the plan should cause it to be reduced.
@ -100,7 +109,7 @@ pub struct Bank {
impl Default for Bank {
fn default() -> Self {
Bank {
balances: RwLock::new(HashMap::new()),
accounts: RwLock::new(HashMap::new()),
pending: RwLock::new(HashMap::new()),
last_ids: RwLock::new(VecDeque::new()),
last_ids_sigs: RwLock::new(HashMap::new()),
@ -121,7 +130,7 @@ impl Bank {
/// Create an Bank using a deposit.
pub fn new_from_deposit(deposit: &Payment) -> Self {
let bank = Self::default();
bank.apply_payment(deposit, &mut bank.balances.write().unwrap());
bank.apply_payment(deposit, &mut bank.accounts.write().unwrap());
bank
}
@ -137,8 +146,11 @@ impl Bank {
}
/// Commit funds to the `payment.to` party.
fn apply_payment(&self, payment: &Payment, balances: &mut HashMap<Pubkey, i64>) {
*balances.entry(payment.to).or_insert(0) += payment.tokens;
fn apply_payment(&self, payment: &Payment, accounts: &mut HashMap<Pubkey, Account>) {
accounts
.entry(payment.to)
.or_insert_with(Account::default)
.tokens += payment.tokens;
}
/// Return the last entry ID registered.
@ -235,10 +247,14 @@ impl Bank {
/// Deduct tokens from the 'from' address the account has sufficient
/// funds and isn't a duplicate.
fn apply_debits(&self, tx: &Transaction, bals: &mut HashMap<Pubkey, i64>) -> Result<()> {
fn apply_debits(
&self,
tx: &Transaction,
accounts: &mut HashMap<Pubkey, Account>,
) -> Result<()> {
let mut purge = false;
{
let option = bals.get_mut(&tx.from);
let option = accounts.get_mut(&tx.from);
if option.is_none() {
// TODO: this is gnarly because the counters are static atomics
if !self.is_leader {
@ -259,19 +275,19 @@ impl Bank {
return Err(BankError::NegativeTokens);
}
if *bal < contract.tokens {
if bal.tokens < contract.tokens {
self.forget_signature_with_last_id(&tx.signature, &tx.last_id);
return Err(BankError::InsufficientFunds(tx.from));
} else if *bal == contract.tokens {
} else if bal.tokens == contract.tokens {
purge = true;
} else {
*bal -= contract.tokens;
bal.tokens -= contract.tokens;
}
};
}
if purge {
bals.remove(&tx.from);
accounts.remove(&tx.from);
}
Ok(())
@ -279,12 +295,12 @@ impl Bank {
/// Apply only a transaction's credits.
/// Note: It is safe to apply credits from multiple transactions in parallel.
fn apply_credits(&self, tx: &Transaction, balances: &mut HashMap<Pubkey, i64>) {
fn apply_credits(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) {
match &tx.instruction {
Instruction::NewContract(contract) => {
let plan = contract.plan.clone();
if let Some(payment) = plan.final_payment() {
self.apply_payment(&payment, balances);
self.apply_payment(&payment, accounts);
} else {
let mut pending = self
.pending
@ -305,13 +321,26 @@ impl Bank {
}
}
}
fn save_data(&self, tx: &Transaction, accounts: &mut HashMap<Pubkey, Account>) {
//TODO This is a temporary implementation until the full rules on memory management for
//smart contracts are implemented. See github issue #953
if !tx.userdata.is_empty() {
if let Some(ref mut account) = accounts.get_mut(&tx.from) {
if account.userdata.len() != tx.userdata.len() {
account.userdata.resize(tx.userdata.len(), 0);
}
account.userdata.copy_from_slice(&tx.userdata);
}
}
}
/// Process a Transaction. If it contains a payment plan that requires a witness
/// to progress, the payment plan will be stored in the bank.
pub fn process_transaction(&self, tx: &Transaction) -> Result<()> {
let bals = &mut self.balances.write().unwrap();
self.apply_debits(tx, bals)?;
self.apply_credits(tx, bals);
let accounts = &mut self.accounts.write().unwrap();
self.apply_debits(tx, accounts)?;
self.apply_credits(tx, accounts);
self.save_data(tx, accounts);
self.transaction_count.fetch_add(1, Ordering::Relaxed);
Ok(())
}
@ -319,13 +348,13 @@ impl Bank {
/// Process a batch of transactions.
#[must_use]
pub fn process_transactions(&self, txs: Vec<Transaction>) -> Vec<Result<Transaction>> {
let bals = &mut self.balances.write().unwrap();
let accounts = &mut self.accounts.write().unwrap();
debug!("processing Transactions {}", txs.len());
let txs_len = txs.len();
let now = Instant::now();
let results: Vec<_> = txs
.into_iter()
.map(|tx| self.apply_debits(&tx, bals).map(|_| tx))
.map(|tx| self.apply_debits(&tx, accounts).map(|_| tx))
.collect(); // Calling collect() here forces all debits to complete before moving on.
let debits = now.elapsed();
@ -335,7 +364,7 @@ impl Bank {
.into_iter()
.map(|result| {
result.map(|tx| {
self.apply_credits(&tx, bals);
self.apply_credits(&tx, accounts);
tx
})
})
@ -467,7 +496,7 @@ impl Bank {
None
}.expect("invalid ledger, needs to start with a contract");
self.apply_payment(&deposit, &mut self.balances.write().unwrap());
self.apply_payment(&deposit, &mut self.accounts.write().unwrap());
}
self.register_entry_id(&entry0.id);
self.register_entry_id(&entry1.id);
@ -498,7 +527,7 @@ impl Bank {
{
e.get_mut().apply_witness(&Witness::Signature, &from);
if let Some(payment) = e.get().final_payment() {
self.apply_payment(&payment, &mut self.balances.write().unwrap());
self.apply_payment(&payment, &mut self.accounts.write().unwrap());
e.remove_entry();
}
};
@ -521,7 +550,7 @@ impl Bank {
for (key, plan) in pending.iter_mut() {
plan.apply_witness(&Witness::Timestamp(dt), &from);
if let Some(payment) = plan.final_payment() {
self.apply_payment(&payment, &mut self.balances.write().unwrap());
self.apply_payment(&payment, &mut self.accounts.write().unwrap());
completed.push(key.clone());
}
}
@ -564,11 +593,15 @@ impl Bank {
}
pub fn get_balance(&self, pubkey: &Pubkey) -> i64 {
let bals = self
.balances
self.get_account(pubkey).map(|a| a.tokens).unwrap_or(0)
}
pub fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
let accounts = self
.accounts
.read()
.expect("'balances' read lock in get_balance");
bals.get(pubkey).cloned().unwrap_or(0)
.expect("'accounts' read lock in get_balance");
accounts.get(pubkey).cloned()
}
pub fn transaction_count(&self) -> usize {
@ -682,6 +715,21 @@ mod tests {
assert_eq!(bank.get_balance(&pubkey), 500);
}
#[test]
fn test_userdata() {
let mint = Mint::new(10_000);
let bank = Bank::new(&mint);
let pubkey = mint.keypair().pubkey();
let mut tx = Transaction::new(&mint.keypair(), pubkey, 0, bank.last_id());
tx.userdata = vec![1, 2, 3];
let rv = bank.process_transaction(&tx);
assert!(rv.is_ok());
let account = bank.get_account(&pubkey);
assert!(account.is_some());
assert_eq!(account.unwrap().userdata, vec![1, 2, 3]);
}
#[test]
fn test_transfer_on_date() {
let mint = Mint::new(1);

View File

@ -91,7 +91,7 @@ fn main() -> () {
};
let mut client = mk_client(&repl_clone);
let previous_balance = client.poll_get_balance(&leader_pubkey).unwrap();
let previous_balance = client.poll_get_balance(&leader_pubkey).unwrap_or(0);
eprintln!("balance is {}", previous_balance);
if previous_balance == 0 {

View File

@ -254,7 +254,7 @@ fn process_command(
"Requesting airdrop of {:?} tokens from {}",
tokens, config.drone_addr
);
let previous_balance = client.poll_get_balance(&config.id.pubkey())?;
let previous_balance = client.poll_get_balance(&config.id.pubkey()).unwrap_or(0);
request_airdrop(&config.drone_addr, &config.id.pubkey(), tokens as u64)?;
// TODO: return airdrop Result from Drone instead of polling the
@ -262,7 +262,10 @@ fn process_command(
let mut current_balance = previous_balance;
for _ in 0..20 {
sleep(Duration::from_millis(500));
current_balance = client.poll_get_balance(&config.id.pubkey())?;
current_balance = client
.poll_get_balance(&config.id.pubkey())
.unwrap_or(previous_balance);
if previous_balance != current_balance {
break;
}

View File

@ -1,12 +1,13 @@
//! The `request` module defines the messages for the thin client.
use bank::Account;
use hash::Hash;
use signature::{Pubkey, Signature};
#[cfg_attr(feature = "cargo-clippy", allow(large_enum_variant))]
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum Request {
GetBalance { key: Pubkey },
GetAccount { key: Pubkey },
GetLastId,
GetTransactionCount,
GetSignature { signature: Signature },
@ -22,9 +23,20 @@ impl Request {
#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
Balance { key: Pubkey, val: i64 },
LastId { id: Hash },
TransactionCount { transaction_count: u64 },
SignatureStatus { signature_status: bool },
Finality { time: usize },
Account {
key: Pubkey,
account: Option<Account>,
},
LastId {
id: Hash,
},
TransactionCount {
transaction_count: u64,
},
SignatureStatus {
signature_status: bool,
},
Finality {
time: usize,
},
}

View File

@ -22,10 +22,10 @@ impl RequestProcessor {
rsp_addr: SocketAddr,
) -> Option<(Response, SocketAddr)> {
match msg {
Request::GetBalance { key } => {
let val = self.bank.get_balance(&key);
let rsp = (Response::Balance { key, val }, rsp_addr);
info!("Response::Balance {:?}", rsp);
Request::GetAccount { key } => {
let account = self.bank.get_account(&key);
let rsp = (Response::Account { key, account }, rsp_addr);
info!("Response::Account {:?}", rsp);
Some(rsp)
}
Request::GetLastId => {

View File

@ -3,6 +3,7 @@
//! messages to the network directly. The binary encoding of its messages are
//! unstable and may change in future releases.
use bank::Account;
use bincode::{deserialize, serialize};
use hash::Hash;
use request::{Request, Response};
@ -27,7 +28,7 @@ pub struct ThinClient {
transactions_socket: UdpSocket,
last_id: Option<Hash>,
transaction_count: u64,
balances: HashMap<Pubkey, i64>,
balances: HashMap<Pubkey, Account>,
signature_status: bool,
finality: Option<usize>,
}
@ -65,9 +66,15 @@ impl ThinClient {
pub fn process_response(&mut self, resp: &Response) {
match *resp {
Response::Balance { key, val } => {
trace!("Response balance {:?} {:?}", key, val);
self.balances.insert(key, val);
Response::Account {
key,
account: Some(ref account),
} => {
trace!("Response account {:?} {:?}", key, account);
self.balances.insert(key, account.clone());
}
Response::Account { key, account: None } => {
debug!("Response account {}: None ", key);
}
Response::LastId { id } => {
trace!("Response last_id {:?}", id);
@ -129,8 +136,8 @@ impl ThinClient {
/// by the network, this method will hang indefinitely.
pub fn get_balance(&mut self, pubkey: &Pubkey) -> io::Result<i64> {
trace!("get_balance");
let req = Request::GetBalance { key: *pubkey };
let data = serialize(&req).expect("serialize GetBalance in pub fn get_balance");
let req = Request::GetAccount { key: *pubkey };
let data = serialize(&req).expect("serialize GetAccount in pub fn get_balance");
self.requests_socket
.send_to(&data, &self.requests_addr)
.expect("buffer error in pub fn get_balance");
@ -138,14 +145,14 @@ impl ThinClient {
while !done {
let resp = self.recv_response()?;
trace!("recv_response {:?}", resp);
if let Response::Balance { key, .. } = &resp {
if let Response::Account { key, .. } = &resp {
done = key == pubkey;
}
self.process_response(&resp);
}
self.balances
.get(pubkey)
.cloned()
.map(|a| a.tokens)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "nokey"))
}

View File

@ -92,15 +92,20 @@ pub struct Transaction {
/// The number of tokens paid for processing and storage of this transaction.
pub fee: i64,
/// Optional user data to be stored in the account
/// TODO: This will be a required field for all contract operations including a simple spend.
/// `instruction` will be serialized into `userdata` once Budget is its own generic contract.
pub userdata: Vec<u8>,
}
impl Transaction {
/// Create a signed transaction from the given `Instruction`.
fn new_from_instruction(
/// Create a signed transaction with userdata and an instruction
fn new_with_userdata_and_instruction(
from_keypair: &Keypair,
instruction: Instruction,
last_id: Hash,
fee: i64,
userdata: Vec<u8>,
) -> Self {
let from = from_keypair.pubkey();
let mut tx = Transaction {
@ -109,10 +114,20 @@ impl Transaction {
last_id,
from,
fee,
userdata,
};
tx.sign(from_keypair);
tx
}
/// Create a signed transaction from the given `Instruction`.
fn new_from_instruction(
from_keypair: &Keypair,
instruction: Instruction,
last_id: Hash,
fee: i64,
) -> Self {
Self::new_with_userdata_and_instruction(from_keypair, instruction, last_id, fee, vec![])
}
/// Create and sign a new Transaction. Used for unit-testing.
pub fn new_taxed(
@ -180,6 +195,9 @@ impl Transaction {
let fee_data = serialize(&(&self.fee)).expect("serialize last_id");
data.extend_from_slice(&fee_data);
let userdata = serialize(&(&self.userdata)).expect("serialize userdata");
data.extend_from_slice(&userdata);
data
}
@ -274,6 +292,7 @@ mod tests {
last_id: Default::default(),
signature: Default::default(),
fee: 0,
userdata: vec![],
};
let buf = serialize(&claim0).unwrap();
let claim1: Transaction = deserialize(&buf).unwrap();
@ -321,6 +340,27 @@ mod tests {
assert_matches!(memfind(&tx_bytes, &tx.signature.as_ref()), Some(SIG_OFFSET));
assert_matches!(memfind(&tx_bytes, &tx.from.as_ref()), Some(PUB_KEY_OFFSET));
}
#[test]
fn test_userdata_layout() {
let mut tx0 = test_tx();
tx0.userdata = vec![1, 2, 3];
let sign_data0a = tx0.get_sign_data();
let tx_bytes = serialize(&tx0).unwrap();
assert!(tx_bytes.len() < 256);
assert_eq!(memfind(&tx_bytes, &sign_data0a), Some(SIGNED_DATA_OFFSET));
assert_eq!(
memfind(&tx_bytes, &tx0.signature.as_ref()),
Some(SIG_OFFSET)
);
assert_eq!(memfind(&tx_bytes, &tx0.from.as_ref()), Some(PUB_KEY_OFFSET));
let tx1 = deserialize(&tx_bytes).unwrap();
assert_eq!(tx0, tx1);
assert_eq!(tx1.userdata, vec![1, 2, 3]);
tx0.userdata = vec![1, 2, 4];
let sign_data0b = tx0.get_sign_data();
assert_ne!(sign_data0a, sign_data0b);
}
#[test]
fn test_overspend_attack() {

View File

@ -177,12 +177,12 @@ fn test_multi_node_ledger_window() -> result::Result<()> {
loop {
let mut client = mk_client(&validator_data);
let bal = client.poll_get_balance(&bob_pubkey)?;
if bal == leader_balance {
let bal = client.poll_get_balance(&bob_pubkey);
info!("bob balance on validator {:?}...", bal);
if bal.unwrap_or(0) == leader_balance {
break;
}
sleep(Duration::from_millis(300));
info!("bob balance on validator {}...", bal);
}
info!("done!");