Skip to content

Commit

Permalink
SVM: Examples: Add PayTube Example (anza-xyz#2474)
Browse files Browse the repository at this point in the history
* SVM: examples: add paytube example

* fix comment

* fix tests
  • Loading branch information
buffalojoec authored and ray-kast committed Nov 27, 2024
1 parent 43af220 commit 610f246
Show file tree
Hide file tree
Showing 13 changed files with 1,013 additions and 2 deletions.
22 changes: 20 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ members = [
"svm",
"svm-conformance",
"svm-transaction",
"svm/examples/paytube",
"test-validator",
"thin-client",
"timings",
Expand Down Expand Up @@ -422,6 +423,7 @@ solana-storage-proto = { path = "storage-proto", version = "=2.1.0" }
solana-streamer = { path = "streamer", version = "=2.1.0" }
solana-svm = { path = "svm", version = "=2.1.0" }
solana-svm-conformance = { path = "svm-conformance", version = "=2.1.0" }
solana-svm-example-paytube = { path = "svm/examples/paytube", version = "=2.1.0" }
solana-svm-transaction = { path = "svm-transaction", version = "=2.1.0" }
solana-system-program = { path = "programs/system", version = "=2.1.0" }
solana-test-validator = { path = "test-validator", version = "=2.1.0" }
Expand Down
22 changes: 22 additions & 0 deletions svm/examples/paytube/Cargo.toml
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 }
18 changes: 18 additions & 0 deletions svm/examples/paytube/README.md
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.
202 changes: 202 additions & 0 deletions svm/examples/paytube/src/lib.rs
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();
}
}
58 changes: 58 additions & 0 deletions svm/examples/paytube/src/loader.rs
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)))
}
}
Loading

0 comments on commit 610f246

Please sign in to comment.