forked from anza-xyz/agave
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SVM: Examples: Add PayTube Example (anza-xyz#2474)
* SVM: examples: add paytube example * fix comment * fix tests
- Loading branch information
1 parent
43af220
commit 610f246
Showing
13 changed files
with
1,013 additions
and
2 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "solana-svm-example-paytube" | ||
description = "Reference example using Solana SVM API" | ||
version = { workspace = true } | ||
edition = { workspace = true } | ||
publish = false | ||
|
||
[dependencies] | ||
solana-bpf-loader-program = { workspace = true } | ||
solana-client = { workspace = true } | ||
solana-compute-budget = { workspace = true } | ||
solana-logger = { workspace = true } | ||
solana-program-runtime = { workspace = true } | ||
solana-sdk = { workspace = true } | ||
solana-svm = { workspace = true } | ||
solana-system-program = { workspace = true } | ||
spl-associated-token-account = { workspace = true } | ||
spl-token = { workspace = true } | ||
termcolor = "1.4.1" | ||
|
||
[dev-dependencies] | ||
solana-test-validator = { workspace = true } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# PayTube | ||
|
||
A reference implementation of an off-chain [state channel](https://ethereum.org/en/developers/docs/scaling/state-channels/) | ||
built using [Anza's SVM API](https://www.anza.xyz/blog/anzas-new-svm-api). | ||
|
||
With the release of Agave 2.0, we've decoupled the SVM API from the rest of the | ||
runtime, which means it can be used outside the validator. This unlocks | ||
SVM-based solutions such as sidecars, channels, rollups, and more. This project | ||
demonstrates everything you need to know about boostrapping with this new API. | ||
|
||
PayTube is a state channel (more specifically a payment channel), designed to | ||
allow multiple parties to transact amongst each other in SOL or SPL tokens | ||
off-chain. When the channel is closed, the resulting changes in each user's | ||
balances are posted to the base chain (Solana). | ||
|
||
Although this project is for demonstration purposes, a payment channel similar | ||
to PayTube could be created that scales to handle massive bandwidth of | ||
transfers, saving the overhead of posting transactions to the chain for last. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
//! PayTube. A simple SPL payment channel. | ||
//! | ||
//! PayTube is an SVM-based payment channel that allows two parties to exchange | ||
//! tokens off-chain. The channel is opened by invoking the PayTube "VM", | ||
//! running on some arbitrary server(s). When transacting has concluded, the | ||
//! channel is closed by submitting the final payment ledger to Solana. | ||
//! | ||
//! The final ledger tracks debits and credits to all registered token accounts | ||
//! or system accounts (native SOL) during the lifetime of a channel. It is | ||
//! then used to to craft a batch of transactions to submit to the settlement | ||
//! chain (Solana). | ||
//! | ||
//! Users opt-in to using a PayTube channel by "registering" their token | ||
//! accounts to the channel. This is done by delegating a token account to the | ||
//! PayTube on-chain program on Solana. This delegation is temporary, and | ||
//! released immediately after channel settlement. | ||
//! | ||
//! Note: This opt-in solution is for demonstration purposes only. | ||
//! | ||
//! ```text | ||
//! | ||
//! PayTube "VM" | ||
//! | ||
//! Bob Alice Bob Alice Will | ||
//! | | | | | | ||
//! | --o--o--o-> | | --o--o--o-> | | | ||
//! | | | | --o--o--o-> | <--- PayTube | ||
//! | <-o--o--o-- | | <-o--o--o-- | | Transactions | ||
//! | | | | | | ||
//! | --o--o--o-> | | -----o--o--o-----> | | ||
//! | | | | | ||
//! | --o--o--o-> | | <----o--o--o------ | | ||
//! | ||
//! \ / \ | / | ||
//! | ||
//! ------ ------ | ||
//! Alice: x Alice: x | ||
//! Bob: x Bob: x <--- Solana Transaction | ||
//! Will: x with final ledgers | ||
//! ------ ------ | ||
//! | ||
//! \\ \\ | ||
//! x x | ||
//! | ||
//! Solana Solana <--- Settled to Solana | ||
//! ``` | ||
//! | ||
//! The Solana SVM's `TransactionBatchProcessor` requires projects to provide a | ||
//! "loader" plugin, which implements the `TransactionProcessingCallback` | ||
//! interface. | ||
//! | ||
//! PayTube defines a `PayTubeAccountLoader` that implements the | ||
//! `TransactionProcessingCallback` interface, and provides it to the | ||
//! `TransactionBatchProcessor` to process PayTube transactions. | ||
|
||
mod loader; | ||
mod log; | ||
mod processor; | ||
mod settler; | ||
pub mod transaction; | ||
|
||
use { | ||
crate::{ | ||
loader::PayTubeAccountLoader, settler::PayTubeSettler, transaction::PayTubeTransaction, | ||
}, | ||
processor::{ | ||
create_transaction_batch_processor, get_transaction_check_results, PayTubeForkGraph, | ||
}, | ||
solana_client::rpc_client::RpcClient, | ||
solana_compute_budget::compute_budget::ComputeBudget, | ||
solana_sdk::{ | ||
feature_set::FeatureSet, fee::FeeStructure, hash::Hash, rent_collector::RentCollector, | ||
signature::Keypair, | ||
}, | ||
solana_svm::transaction_processor::{ | ||
TransactionProcessingConfig, TransactionProcessingEnvironment, | ||
}, | ||
std::sync::{Arc, RwLock}, | ||
transaction::create_svm_transactions, | ||
}; | ||
|
||
/// A PayTube channel instance. | ||
/// | ||
/// Facilitates native SOL or SPL token transfers amongst various channel | ||
/// participants, settling the final changes in balances to the base chain. | ||
pub struct PayTubeChannel { | ||
/// I think you know why this is a bad idea... | ||
keys: Vec<Keypair>, | ||
rpc_client: RpcClient, | ||
} | ||
|
||
impl PayTubeChannel { | ||
pub fn new(keys: Vec<Keypair>, rpc_client: RpcClient) -> Self { | ||
Self { keys, rpc_client } | ||
} | ||
|
||
/// The PayTube API. Processes a batch of PayTube transactions. | ||
/// | ||
/// Obviously this is a very simple implementation, but one could imagine | ||
/// a more complex service that employs custom functionality, such as: | ||
/// | ||
/// * Increased throughput for individual P2P transfers. | ||
/// * Custom Solana transaction ordering (e.g. MEV). | ||
/// | ||
/// The general scaffold of the PayTube API would remain the same. | ||
pub fn process_paytube_transfers(&self, transactions: &[PayTubeTransaction]) { | ||
log::setup_solana_logging(); | ||
log::creating_paytube_channel(); | ||
|
||
// PayTube default configs. | ||
// | ||
// These can be configurable for channel customization, including | ||
// imposing resource or feature restrictions, but more commonly they | ||
// would likely be hoisted from the cluster. | ||
// | ||
// For example purposes, they are provided as defaults here. | ||
let compute_budget = ComputeBudget::default(); | ||
let feature_set = FeatureSet::all_enabled(); | ||
let fee_structure = FeeStructure::default(); | ||
let lamports_per_signature = fee_structure.lamports_per_signature; | ||
let rent_collector = RentCollector::default(); | ||
|
||
// PayTube loader/callback implementation. | ||
// | ||
// Required to provide the SVM API with a mechanism for loading | ||
// accounts. | ||
let account_loader = PayTubeAccountLoader::new(&self.rpc_client); | ||
|
||
// Solana SVM transaction batch processor. | ||
// | ||
// Creates an instance of `TransactionBatchProcessor`, which can be | ||
// used by PayTube to process transactions using the SVM. | ||
// | ||
// This allows programs such as the System and Token programs to be | ||
// translated and executed within a provisioned virtual machine, as | ||
// well as offers many of the same functionality as the lower-level | ||
// Solana runtime. | ||
let fork_graph = Arc::new(RwLock::new(PayTubeForkGraph {})); | ||
let processor = create_transaction_batch_processor( | ||
&account_loader, | ||
&feature_set, | ||
&compute_budget, | ||
Arc::clone(&fork_graph), | ||
); | ||
|
||
// The PayTube transaction processing runtime environment. | ||
// | ||
// Again, these can be configurable or hoisted from the cluster. | ||
let processing_environment = TransactionProcessingEnvironment { | ||
blockhash: Hash::default(), | ||
epoch_total_stake: None, | ||
epoch_vote_accounts: None, | ||
feature_set: Arc::new(feature_set), | ||
fee_structure: Some(&fee_structure), | ||
lamports_per_signature, | ||
rent_collector: Some(&rent_collector), | ||
}; | ||
|
||
// The PayTube transaction processing config for Solana SVM. | ||
// | ||
// Extended configurations for even more customization of the SVM API. | ||
let processing_config = TransactionProcessingConfig { | ||
compute_budget: Some(compute_budget), | ||
..Default::default() | ||
}; | ||
|
||
// Step 1: Convert the batch of PayTube transactions into | ||
// SVM-compatible transactions for processing. | ||
// | ||
// In the future, the SVM API may allow for trait-based transactions. | ||
// In this case, `PayTubeTransaction` could simply implement the | ||
// interface, and avoid this conversion entirely. | ||
let svm_transactions = create_svm_transactions(transactions); | ||
|
||
// Step 2: Process the SVM-compatible transactions with the SVM API. | ||
log::processing_transactions(svm_transactions.len()); | ||
let results = processor.load_and_execute_sanitized_transactions( | ||
&account_loader, | ||
&svm_transactions, | ||
get_transaction_check_results(svm_transactions.len(), lamports_per_signature), | ||
&processing_environment, | ||
&processing_config, | ||
); | ||
|
||
// Step 3: Convert the SVM API processor results into a final ledger | ||
// using `PayTubeSettler`, and settle the resulting balance differences | ||
// to the Solana base chain. | ||
// | ||
// Here the settler is basically iterating over the transaction results | ||
// to track debits and credits, but only for those transactions which | ||
// were executed succesfully. | ||
// | ||
// The final ledger of debits and credits to each participant can then | ||
// be packaged into a minimal number of settlement transactions for | ||
// submission. | ||
let settler = PayTubeSettler::new(&self.rpc_client, transactions, results, &self.keys); | ||
log::settling_to_base_chain(settler.num_transactions()); | ||
settler.process_settle(); | ||
|
||
log::channel_closed(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
//! PayTube's "account loader" component, which provides the SVM API with the | ||
//! ability to load accounts for PayTube channels. | ||
//! | ||
//! The account loader is a simple example of an RPC client that can first load | ||
//! an account from the base chain, then cache it locally within the protocol | ||
//! for the duration of the channel. | ||
|
||
use { | ||
solana_client::rpc_client::RpcClient, | ||
solana_sdk::{ | ||
account::{AccountSharedData, ReadableAccount}, | ||
pubkey::Pubkey, | ||
}, | ||
solana_svm::transaction_processing_callback::TransactionProcessingCallback, | ||
std::{collections::HashMap, sync::RwLock}, | ||
}; | ||
|
||
/// An account loading mechanism to hoist accounts from the base chain up to | ||
/// an active PayTube channel. | ||
/// | ||
/// Employs a simple cache mechanism to ensure accounts are only loaded once. | ||
pub struct PayTubeAccountLoader<'a> { | ||
cache: RwLock<HashMap<Pubkey, AccountSharedData>>, | ||
rpc_client: &'a RpcClient, | ||
} | ||
|
||
impl<'a> PayTubeAccountLoader<'a> { | ||
pub fn new(rpc_client: &'a RpcClient) -> Self { | ||
Self { | ||
cache: RwLock::new(HashMap::new()), | ||
rpc_client, | ||
} | ||
} | ||
} | ||
|
||
/// Implementation of the SVM API's `TransactionProcessingCallback` interface. | ||
/// | ||
/// The SVM API requires this plugin be provided to provide the SVM with the | ||
/// ability to load accounts. | ||
/// | ||
/// In the Agave validator, this implementation is Bank, powered by AccountsDB. | ||
impl TransactionProcessingCallback for PayTubeAccountLoader<'_> { | ||
fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<AccountSharedData> { | ||
if let Some(account) = self.cache.read().unwrap().get(pubkey) { | ||
return Some(account.clone()); | ||
} | ||
|
||
let account: AccountSharedData = self.rpc_client.get_account(pubkey).ok()?.into(); | ||
self.cache.write().unwrap().insert(*pubkey, account.clone()); | ||
|
||
Some(account) | ||
} | ||
|
||
fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option<usize> { | ||
self.get_account_shared_data(account) | ||
.and_then(|account| owners.iter().position(|key| account.owner().eq(key))) | ||
} | ||
} |
Oops, something went wrong.