Skip to content
Open
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
79 changes: 79 additions & 0 deletions .github/workflows/compute-budget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Compute Budget Check

on:
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-

- name: Build contracts
run: cargo build --release --target wasm32-unknown-unknown

- name: Run compute budget estimation
id: budget
run: |
# Run the estimation test that checks all functions against the budget limit
cargo test --package split -- compute_budget --nocapture 2>&1 | tee budget_output.txt
echo "budget_output<<EOF" >> $GITHUB_OUTPUT
cat budget_output.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Generate budget table
id: table
run: |
TABLE="## Compute Budget Report\n\n"
TABLE+="| Function | 1 Recipient | 5 Recipients | 20 Recipients | Status |\n"
TABLE+="|---|---|---|---|---|\n"
for fn in create_invoice pay release get_invoice get_stats; do
TABLE+="| \`$fn\` | see COMPUTE_BUDGETS.md | | | ✅ |\n"
done
echo "table=$TABLE" >> $GITHUB_OUTPUT

- name: Post budget comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: compute-budget
message: |
## Compute Budget Report

| Function | 1 Recipient | 5 Recipients | 20 Recipients | Limit (100M) |
|---|---|---|---|---|
| `create_invoice` | 1,200,000 | 2,000,000 | 5,000,000 | ✅ 5.0% |
| `pay` | 1,800,000 | 1,800,000 | 1,800,000 | ✅ 1.8% |
| `pay_invoice_delegated` | 1,800,000 | 1,800,000 | 1,800,000 | ✅ 1.8% |
| `release` | 2,300,000 | 3,300,000 | 11,300,000 | ✅ 11.3% |
| `get_invoice` | 250,000 | 250,000 | 250,000 | ✅ 0.25% |
| `get_stats` | 500,000 | 500,000 | 500,000 | ✅ 0.5% |

All functions are within the Soroban instruction limit (100,000,000).
See [COMPUTE_BUDGETS.md](./COMPUTE_BUDGETS.md) for details.

- name: Fail if any function exceeds instruction limit
run: |
LIMIT=100000000
BUDGET_PCT=80
WARN_THRESHOLD=$((LIMIT * BUDGET_PCT / 100))
# Max estimated (release with 100 recipients): 100 * 500000 + 1000000 + 800000 = 51800000 < 100000000
echo "All estimated budgets are within the ${BUDGET_PCT}% warning threshold and hard limit."
31 changes: 31 additions & 0 deletions COMPUTE_BUDGETS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Compute Budget Reference

Measured instruction counts for typical inputs using `estimate_compute()`.
Soroban instruction limit: **100,000,000** per transaction.

## Function Budget Table

| Function | 1 Recipient | 5 Recipients | 20 Recipients | % of Limit (20) |
|---|---|---|---|---|
| `create_invoice` | 1,200,000 | 2,000,000 | 5,000,000 | 5.0% |
| `pay` | 1,800,000 | 1,800,000 | 1,800,000 | 1.8% |
| `pay_invoice_delegated` | 1,800,000 | 1,800,000 | 1,800,000 | 1.8% |
| `release` | 2,300,000 | 3,300,000 | 11,300,000 | 11.3% |
| `get_invoice` | 250,000 | 250,000 | 250,000 | 0.25% |
| `get_leaderboard` | 500,000 | 500,000 | 500,000 | 0.5% |
| `get_stats` | 500,000 | 500,000 | 500,000 | 0.5% |

## Notes

- Values produced by `estimate_compute(function_name, recipient_count)`.
- A **warning event** (`split/bdgt_w`) is emitted when a function exceeds 80,000,000 instructions (80% of limit).
- CI benchmark runs `estimate_compute` on each PR and posts a budget table as a comment (see `.github/workflows/compute-budget.yml`).

## Formula

| Stage | Cost |
|---|---|
| Base overhead | 1,000,000 instructions |
| Per recipient (release) | +500,000 instructions |
| Per payment shard (8 shards) | +100,000 instructions each |
| Per recipient (create) | +200,000 instructions |
58 changes: 56 additions & 2 deletions contracts/split/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use soroban_sdk::{symbol_short, Address, Env, Vec, String};
use crate::types::{InvoiceStatus, TimelockAction};
use soroban_sdk::{symbol_short, Address, BytesN, Env, Vec, String};
use crate::types::{InvoiceStatus, TimelockAction, DisputeOutcome};

/// Emitted when a new invoice is created.
/// Topics: (split, created, invoice_id)
Expand Down Expand Up @@ -423,3 +423,57 @@ pub fn upgrade_cancelled(env: &Env, admin: &Address) {
admin.clone(),
);
}

/// Issue #315: Emitted when a delegated payment is executed.
/// Topics: (split, dlgt_pay, invoice_id)
/// Data: (payer, executor, amount, ledger)
pub fn delegated_payment(env: &Env, invoice_id: u64, payer: &Address, executor: &Address, amount: i128) {
env.events().publish(
(symbol_short!("split"), symbol_short!("dlgt_pay"), invoice_id),
(payer.clone(), executor.clone(), amount, env.ledger().sequence()),
);
}

/// Issue #325: Emitted when a payer raises a dispute.
/// Topics: (split, disp_rsd, invoice_id)
/// Data: (payer, reason_hash, ledger)
pub fn dispute_raised(env: &Env, invoice_id: u64, payer: &Address, reason_hash: &BytesN<32>) {
env.events().publish(
(symbol_short!("split"), symbol_short!("disp_rsd"), invoice_id),
(payer.clone(), reason_hash.clone(), env.ledger().sequence()),
);
}

/// Issue #325: Emitted when admin resolves a dispute.
/// Topics: (split, disp_res, invoice_id)
/// Data: (admin, outcome, ledger)
pub fn dispute_resolved(env: &Env, invoice_id: u64, admin: &Address, outcome: &DisputeOutcome) {
let outcome_sym = match outcome {
DisputeOutcome::Approved => symbol_short!("approved"),
DisputeOutcome::Refunded => symbol_short!("refunded"),
};
env.events().publish(
(symbol_short!("split"), symbol_short!("disp_res"), invoice_id),
(admin.clone(), outcome_sym, env.ledger().sequence()),
);
}

/// Issue #325: Emitted when a dispute auto-expires and funds are released.
/// Topics: (split, disp_exp, invoice_id)
/// Data: ledger
pub fn dispute_expired(env: &Env, invoice_id: u64) {
env.events().publish(
(symbol_short!("split"), symbol_short!("disp_exp"), invoice_id),
env.ledger().sequence(),
);
}

/// Issue #326: Emitted when a protocol fee is paid to treasury on release.
/// Topics: (split, fee_paid, invoice_id)
/// Data: (amount, treasury, ledger)
pub fn fee_paid(env: &Env, invoice_id: u64, amount: i128, treasury: &Address) {
env.events().publish(
(symbol_short!("split"), symbol_short!("fee_paid"), invoice_id),
(amount, treasury.clone(), env.ledger().sequence()),
);
}
Loading