2026-03-26 12:51:53
Your arbitrage bot spotted a 2% price difference between Solana and Ethereum. The window's closing fast — but your bot has to juggle wallet connections across Jupiter, Uniswap, and LI.FI for the bridge. By the time you've signed three transactions and paid $40 in gas, the opportunity's gone. Sound familiar?
Most trading bots are built like Formula 1 cars with bicycle wheels. The strategy engine is sophisticated — multi-protocol arbitrage, MEV extraction, yield farming automation — but the wallet layer is a mess of scattered API keys, manual gas management, and prayer-based transaction signing.
You end up writing the same wallet infrastructure over and over: transaction queueing, gas price monitoring, balance tracking across chains, retry logic when RPCs go down. Meanwhile, your actual alpha-generating code gets maybe 20% of your development time.
The stakes are real. In high-frequency trading environments, a 200ms delay in transaction execution can mean the difference between profit and loss. Gas optimization isn't just about saving fees — it's about execution speed when network congestion spikes.
WAIaaS solves this with a comprehensive wallet API designed specifically for automated trading systems. Instead of managing keys, RPCs, and transaction signing yourself, your bot gets 39 REST endpoints that handle everything from basic balance queries to complex multi-step DeFi operations.
Here's what your trading bot actually needs to do its job:
Your arbitrage bot shouldn't execute when gas costs eat the spread. WAIaaS includes gas conditional execution as a built-in feature:
curl -X POST http://127.0.0.1:3100/v1/transactions/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"type": "TRANSFER",
"to": "recipient-address",
"amount": "0.1",
"gasCondition": {
"maxGasPrice": "20000000000",
"timeoutSeconds": 300
}
}'
The transaction sits in the pipeline until gas prices drop below your threshold, then executes automatically. Your bot focuses on finding opportunities; WAIaaS handles the execution timing.
Trading bots need to access liquidity wherever it exists. WAIaaS integrates 14 DeFi protocols including Jupiter, Uniswap, Aave, Drift, Hyperliquid, and LI.FI. Same authentication, same response format, same error handling:
# Swap on Jupiter (Solana)
curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1000000000"
}'
# Open leveraged position on Hyperliquid
curl -X POST http://127.0.0.1:3100/v1/actions/hyperliquid/place-order \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"coin": "ETH",
"is_buy": true,
"sz": "1.5",
"limit_px": "3200",
"order_type": {"limit": {"tif": "Gtc"}}
}'
No more maintaining separate SDK integrations for each protocol. Your bot makes the same HTTP calls whether it's trading perpetuals on Hyperliquid or swapping tokens on Jupiter.
Cross-chain arbitrage requires reliable bridging. WAIaaS includes LI.FI and Across protocol integrations with automatic bridge route optimization:
# Bridge USDC from Ethereum to Solana for arbitrage
curl -X POST http://127.0.0.1:3100/v1/actions/lifi/bridge \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"fromChain": "ethereum",
"toChain": "solana",
"fromToken": "0xA0b86a33E6417Eb9fd3BBB7C8e17b0ad98c6b9E0",
"toToken": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1000000000"
}'
The bridge transaction is automatically included in your bot's transaction pipeline with the same gas conditioning and approval flows as any other operation.
In automated trading, failed transactions aren't just annoying — they cost gas and miss opportunities. WAIaaS includes dry-run simulation for every transaction type:
curl -X POST http://127.0.0.1:3100/v1/transactions/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"type": "TRANSFER",
"to": "recipient-address",
"amount": "0.1",
"dryRun": true
}'
Your bot can simulate the entire transaction sequence, get exact gas estimates, and verify execution before committing capital. Critical for MEV bots where transaction reversion means lost gas with no profit.
For high-frequency strategies, ERC-4337 Account Abstraction enables gasless transactions and batch execution:
# Build UserOp for gasless execution
curl -X POST http://127.0.0.1:3100/v1/userop/build \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"transactions": [
{"type": "APPROVE", "spender": "0x...", "amount": "1000000"},
{"type": "CONTRACT_CALL", "to": "0x...", "data": "0x..."}
],
"paymasterService": "pimlico"
}'
Batch multiple operations into a single UserOp, use paymasters for gasless execution, or enable wallet recovery without seed phrases.
Trading bots need safety rails that don't interfere with legitimate strategies. WAIaaS implements a 3-layer security model designed for automated systems:
Your bot gets a JWT session token with configurable limits:
curl -X POST http://127.0.0.1:3100/v1/sessions \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"maxRenewals": 1000,
"ttlSeconds": 3600,
"absoluteLifetimeSeconds": 86400
}'
Sessions can be configured per strategy — your scalping bot gets short TTL with high renewal limits, while your yield farming bot gets longer sessions with fewer renewals.
Policies define spending limits and approval thresholds without breaking automated execution:
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "SPENDING_LIMIT",
"rules": {
"instant_max_usd": 1000, # Execute immediately under $1k
"notify_max_usd": 5000, # Send notification $1k-$5k
"delay_max_usd": 20000, # 15min delay $5k-$20k
"delay_seconds": 900,
"daily_limit_usd": 50000 # Hard daily limit
}
}'
Small trades execute instantly. Large trades get time delays for human review. Your bot keeps running while the policy engine prevents catastrophic losses.
When something goes wrong, you need an emergency stop that works even if your bot is compromised:
# Emergency stop all bot activity
curl -X POST http://127.0.0.1:3100/v1/sessions/<session-id>/revoke \
-H "X-Owner-Signature: <ed25519-signature>" \
-H "X-Owner-Message: <signed-message>"
Owner authentication bypasses compromised bot credentials and immediately stops all transaction activity.
WAIaaS processes transactions through a 7-stage pipeline: validation → authentication → policy → wait → execute → confirm → monitoring. Each stage is optimized for low latency while maintaining security guarantees.
Failed transactions at any stage return detailed error codes your bot can handle programmatically:
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Balance 0.5 SOL insufficient for transaction requiring 0.8 SOL",
"domain": "VALIDATION",
"retryable": false
}
}
WAIaaS handles RPC endpoint management, failover, and load balancing across 15 networks. Your bot makes the same API calls whether targeting Ethereum mainnet, Solana, or Arbitrum. No more managing separate RPC configurations or handling network-specific errors.
Trading bots need accurate balance information for position sizing:
# Get current balance across all tokens
curl http://127.0.0.1:3100/v1/wallet/balance \
-H "Authorization: Bearer wai_sess_<token>"
# Get DeFi positions across all protocols
curl http://127.0.0.1:3100/v1/wallet/defi-positions \
-H "Authorization: Bearer wai_sess_<token>"
Responses include USD valuations, lending positions, liquidity provider stakes, and perpetual futures positions across all integrated protocols.
Get a complete wallet infrastructure running in under 5 minutes:
git clone https://github.com/minhoyoo-iotrust/WAIaaS.git
cd WAIaaS
docker compose up -d
npm install -g @waiaas/cli
waiaas init --auto-provision
waiaas start
waiaas quickset --mode mainnet
Configure your bot: Replace your wallet management code with HTTP calls to http://127.0.0.1:3100. The complete OpenAPI spec is available at http://127.0.0.1:3100/doc.
Set policies: Configure risk limits that match your strategy requirements.
Start trading: Your bot now has enterprise-grade wallet infrastructure with gas optimization, cross-chain support, and built-in risk management.
WAIaaS isn't a prototype — it's a complete financial infrastructure with 611+ test files, Docker deployment, comprehensive monitoring, and production security practices. Trading bots using WAIaaS are already executing millions in volume across Solana and EVM chains.
Ready to upgrade your trading bot's infrastructure? Check out the complete codebase at GitHub or explore the full documentation at waiaas.ai.
2026-03-26 12:50:43
Flat query param filtering breaks down fast. ?status=active&age_gte=18 works until someone needs (status=active OR status=pending) AND age>=18. At that point you're either inventing your own encoding convention or telling the client it can't do that.
The underlying issue is that boolean logic is recursive. AND and OR nest arbitrarily, and flat key-value params have no way to express that structure.
A Context-Free Grammar (CFG) defines a language through production rules. Each rule describes how a named concept (non-terminal) expands into tokens (terminals) or other non-terminals.
The arithmetic grammar is the classic example:
expression → term ( ("+" | "-") term )*
term → factor ( ("*" | "/") factor )*
factor → NUMBER | "(" expression ")"
Operator precedence isn't hardcoded, it falls out of the nesting. term sits inside expression, so * binds tighter than + automatically. factor → "(" expression ")" lets expressions nest to any depth. The grammar describes the full language in a handful of rules, and a parser derived from it handles every valid input correctly by construction.
That last point is the main reason to use a grammar over ad-hoc parsing code. You're not handling cases, you're defining a structure that makes invalid inputs unrepresentable.
Before you can parse, you need a lexer, something that turns a raw string into a stream of tokens. In nestjs-filter-grammar this is done with Chevrotain, a parser building toolkit for TypeScript.
Tokens are defined with createToken. The order they appear in matters. Chevrotain matches greedily, so multi-character operators need to come before their single-character prefixes:
import { createToken, Lexer } from 'chevrotain';
// Multi-char operators first
export const GreaterEqual = createToken({ name: 'GreaterEqual', pattern: />=/ });
export const LessEqual = createToken({ name: 'LessEqual', pattern: /<=/ });
export const NotEqual = createToken({ name: 'NotEqual', pattern: /!=/ });
export const IContains = createToken({ name: 'IContains', pattern: /\*~/ });
export const Contains = createToken({ name: 'Contains', pattern: /\*=/ });
export const IStartsWith = createToken({ name: 'IStartsWith', pattern: /\^~/ });
export const StartsWith = createToken({ name: 'StartsWith', pattern: /\^=/ });
export const IEndsWith = createToken({ name: 'IEndsWith', pattern: /\$~/ });
export const EndsWith = createToken({ name: 'EndsWith', pattern: /\$=/ });
export const INotEqual = createToken({ name: 'INotEqual', pattern: /!~/ });
// Single-char operators after
export const GreaterThan = createToken({ name: 'GreaterThan', pattern: />/ });
export const LessThan = createToken({ name: 'LessThan', pattern: /</ });
export const IEquals = createToken({ name: 'IEquals', pattern: /~/ });
export const Equals = createToken({ name: 'Equals', pattern: /=/ });
// Structural tokens
export const Semicolon = createToken({ name: 'Semicolon', pattern: /;/ }); // AND
export const Pipe = createToken({ name: 'Pipe', pattern: /\|/ }); // OR
export const Comma = createToken({ name: 'Comma', pattern: /,/ });
export const LParen = createToken({ name: 'LParen', pattern: /\(/ });
export const RParen = createToken({ name: 'RParen', pattern: /\)/ });
// Literals
export const StringLiteral = createToken({
name: 'StringLiteral',
pattern: /"(?:[^"\\]|\\.)*"/,
});
export const Token = createToken({
name: 'Token',
pattern: /[^,"|;!=><^$\*~()\s]+/,
});
// NullLiteral uses longer_alt so "nullable" still matches Token
export const NullLiteral = createToken({
name: 'NullLiteral',
pattern: /null/,
longer_alt: Token,
});
export const WhiteSpace = createToken({
name: 'WhiteSpace',
pattern: /\s+/,
group: Lexer.SKIPPED,
});
The longer_alt on NullLiteral is worth noting. Without it, a field named nullable would partially match as null followed by able. Chevrotain's longer_alt says: only match this token if there isn't a longer match available for the alternative token.
With tokens defined, the grammar itself describes how those tokens compose into valid filter expressions:
filter → or_expr
or_expr → and_expr ( "|" and_expr )*
and_expr → atom ( ";" atom )*
atom → condition | "(" or_expr ")"
condition → field operator values
values → value ( "," value )*
value → Token | StringLiteral | NullLiteral
AND binds tighter than OR for the same reason * binds tighter than + in arithmetic. and_expr is nested inside or_expr. The atom → "(" or_expr ")" rule is where arbitrary grouping comes from. Comma-separated values in values naturally handles IN queries without any special casing.
Some valid inputs from this grammar:
status=active
status=active;age>=18
status=active|status=pending
(status=active|status=pending);age>=18
status=active,pending
name="John Doe"
name*~john
deletedAt=null
The standard way to implement a parser from a CFG like this is recursive descent, where each non-terminal becomes a function, the structure of the functions mirrors the grammar, and mutual recursion is what handles nesting. Chevrotain generates this parser from the grammar rules you define.
When the parser hits (, it recurses back up to or_expr. The call stack is the implicit parse stack. This is why recursive descent parsers map so naturally to CFGs. The language's recursive structure and the code's recursive structure are the same thing.
The output is a parse tree (AST):
(status=active|status=pending);age>=18
AND
├── OR
│ ├── condition: status = active
│ └── condition: status = pending
└── condition: age >= 18
This tree is what gets handed to the ORM adapter. Translating it to a Prisma where or TypeORM QueryBuilder is a recursive walk. and nodes become AND: [...], or nodes become OR: [...], leaf conditions become field predicates. The parser and the ORM adapter don't know about each other; the tree is the contract between them.
This is the approach behind nestjs-filter-grammar. The packages are:
@nestjs-filter-grammar/core: grammar parser, decorators, validation, Swagger/OpenAPI extensions@nestjs-filter-grammar/typeorm: TypeORM SelectQueryBuilder adapter@nestjs-filter-grammar/prisma: Prisma where/orderBy adapter@nestjs-filter-grammar/mikroorm: MikroORM FilterQuery/QueryOrderMap adapter@nestjs-filter-grammar/client-query-builder: type-safe client query builderThe library adds two things on top of the raw grammar: field validation and ORM adapters.
Field validation is necessary because a raw parser accepts any field name and operator. You need to reject requests that filter on undeclared fields, use operators the field doesn't support, or pass values that can't be coerced to the right type. This is handled with decorators:
@Filterable()
class UserQuery {
@FilterableColumn([FilterOperator.eq, FilterOperator.iContains])
@SortableColumn()
name!: string;
@FilterableColumn([FilterOperator.eq, FilterOperator.neq], { type: Status })
@SortableColumn()
status!: Status;
@FilterableColumn([FilterOperator.gte, FilterOperator.lte], { type: 'number' })
@SortableColumn()
age!: number;
}
You're explicitly declaring which fields are filterable and which operators each one accepts. After parsing, each condition node in the tree is checked against this schema before anything reaches the ORM.
The controller side uses a @Filter() decorator that parses the query string and runs validation:
@Controller('users')
export class UsersController {
@Get()
findAll(@Filter(UserQuery) { filter, sort, query }: FilterResult) {
// filter is a validated parse tree, or undefined
// sort is a parsed list of sort entries, or undefined
}
}
ORM adapters take the validated tree and produce queries. For Prisma:
import { applyFilter, applySort } from '@nestjs-filter-grammar/prisma';
const users = await prisma.user.findMany({
where: filter ? applyFilter(filter) : undefined,
orderBy: sort ? applySort(sort) : undefined,
});
Same pattern for TypeORM and MikroORM, different adapter package, same interface.
The grammar being formal means it can be exposed to clients in a type-safe way. @nestjs-filter-grammar/core automatically enriches your OpenAPI spec with x-filter-grammar extensions when @nestjs/swagger is installed. These extensions describe exactly which fields are filterable, which operators each supports, and what types they expect.
@nestjs-filter-grammar/client-query-builder reads those extensions and generates typed filter builders:
import { and, or, sort } from '@nestjs-filter-grammar/client-query-builder';
import { FilterUsers, SortUsers } from './generated';
const filter = and(
or(
FilterUsers.status.eq('active'),
FilterUsers.status.eq('pending'),
),
FilterUsers.age.gte(18),
).build();
// → "(status=active|status=pending);age>=18"
const sortStr = sort(SortUsers.name.asc(), SortUsers.age.desc()).build();
// → "+name,-age"
fetch(`/api/users?filter=${filter}&sort=${sortStr}`);
The codegen step reads your OpenAPI spec:
npx filter-grammar generate ./openapi.json -o ./src/generated
Orval is one example of an OpenAPI client generator you can pair this with. Orval generates your API client hooks (React Query, SWR, Axios etc.) and filter-grammar generate generates the typed filter builders from the same spec. But any OpenAPI client generator works; the filter builder generation is a separate step that only needs the spec. The point is that the filter language your server accepts gets surfaced to the client with full type coverage, without maintaining a separate contract.
Sorting is a simpler case, no boolean logic needed, just a list of fields with direction:
sort → sort_entry ( "," sort_entry )*
sort_entry → direction? field
direction → "+" | "-"
sort=+name,-createdAt means name ascending, createdAt descending. + is the default so it can be omitted. This is a linear scan with no recursion.
2026-03-26 12:41:56
TL;DR: Solana's Localized Fee Markets (LFM) solved global congestion — but introduced a surgical denial-of-service vector. By flooding write-locks on a single protocol's state accounts, an attacker can price out keeper bots during the exact moments liquidations matter most. We break down the attack mechanics, show real detection patterns, and provide a state-sharding migration guide.
When Solana introduced Localized Fee Markets via SIMD-0110, it was hailed as an elegant solution to network-wide congestion. Instead of every transaction competing in a single global fee auction, fees became localized — you only paid premium rates when contending for the same state accounts as other transactions.
The theory was sound: a spike in NFT minting shouldn't make your DeFi swap expensive. Each "hot" account gets its own micro-market.
But here's the thing about micro-markets: they can be cornered.
Consider a typical Solana DeFi lending protocol — let's call it LendProtocol. Like most Solana programs, it uses a handful of global state accounts:
GlobalState PDA → Tracks total deposits, borrows, utilization
MarketReserve PDA → Per-asset reserve data
OracleBuffer PDA → Cached price feeds
LiquidationQueue PDA → Pending liquidation orders
Every liquidation, deposit, borrow, and repay instruction needs a write lock on at least GlobalState and MarketReserve. This means all these transactions are competing in the same Localized Fee Market.
Reconnaissance: The attacker maps the protocol's write-lock dependency graph. They identify the 2-3 PDAs that appear as writable accounts in >80% of the protocol's transactions.
Trigger Detection: The attacker monitors CEX prices (via websocket feeds to Binance, OKX, etc.) for volatile moves that will trigger on-chain liquidations once oracle prices update.
Fee Floor Flooding: When a liquidation-triggering price move is detected, the attacker submits hundreds of no-op transactions that request write locks on the protocol's global state PDAs:
// Attacker's "noisy" program
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_data: &[u8],
) -> ProgramResult {
// Request write lock on target PDAs
// Do nothing. Just occupy the fee market.
Ok(())
}
Each transaction includes a high priority fee — but the attacker doesn't need to outbid everyone forever. They just need to push the localized base fee high enough that keeper bots' pre-configured fee caps are exceeded.
This is the terrifying part. Because Localized Fee Markets only affect transactions touching the same state accounts, the attacker's cost is dramatically lower than a global DoS:
A few thousand dollars per minute to selectively freeze a protocol holding $100M+ in user funds. The economics are heavily in the attacker's favor.
The 2026 rollout of Firedancer makes this worse. Under the Alpenglow consensus model with SIMD-0370's dynamic block sizing, high-performance leaders can produce blocks with hundreds of millions of Compute Units. This means:
A liquidation bot that waits for Finalized commitment (the safe choice) now faces 2-3x longer confirmation times during periods of validator heterogeneity. Combined with LFM DoS, the keeper is both priced out and unsure when their transaction will actually land.
Monitor your protocol's localized base fee independently from the global base fee:
# Pseudo-code for LFM anomaly detection
def detect_noisy_neighbor(protocol_accounts, window_slots=10):
local_fee = get_localized_base_fee(protocol_accounts, window_slots)
global_fee = get_global_base_fee(window_slots)
ratio = local_fee / max(global_fee, 1)
if ratio > 5.0: # Local fee 5x+ above global
alert(f"LFM anomaly: local/global ratio = {ratio:.1f}")
# Check if spike correlates with oracle volatility
oracle_vol = get_price_volatility(window_slots)
if ratio > 3.0 and oracle_vol > THRESHOLD:
alert("CRITICAL: Possible Noisy Neighbor during volatility event")
Track unique signers requesting write-locks on your PDAs:
def detect_write_lock_concentration(pda_pubkey, window_slots=5):
txns = get_recent_write_lock_txns(pda_pubkey, window_slots)
signer_counts = Counter(tx.signer for tx in txns)
total = len(txns)
# If top 3 signers account for >60% of write locks
top3_share = sum(c for _, c in signer_counts.most_common(3)) / total
if top3_share > 0.6:
alert(f"Write-lock concentration: top 3 signers = {top3_share:.0%}")
Attacker transactions have a distinctive profile — they request write locks but perform minimal computation:
The fundamental fix is reducing write-lock contention on global state. Here's the migration path:
# Using Helius API to map write-lock hotspots
curl -s "https://api.helius.xyz/v0/addresses/{PROGRAM_ID}/transactions?api-key=KEY&limit=1000" \
| jq '[.[] | .accountData[] | select(.writable == true) | .account] | group_by(.) | map({account: .[0], count: length}) | sort_by(-.count) | .[0:5]'
Any account appearing as writable in >10% of your traffic is a high-severity LDoS target.
Replace single-PDA global state with user-derived shards:
// BEFORE: Single global state (LDoS vulnerable)
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, seeds = [b"global_state"], bump)]
pub global_state: Account<'info, GlobalState>,
// ...
}
// AFTER: Sharded state (LDoS resistant)
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(
mut,
seeds = [b"user_shard", user.key().as_ref()],
bump
)]
pub user_shard: Account<'info, UserShard>,
// Global aggregation happens asynchronously via cranks
}
Keeper bots must dynamically respond to localized fee spikes:
async def submit_liquidation(tx, protocol_accounts):
base_fee = await get_localized_fee(protocol_accounts)
# Exponential escalation with economic ceiling
max_profitable_fee = calculate_liquidation_profit(tx) * 0.8
for attempt in range(MAX_ATTEMPTS):
priority_fee = min(
base_fee * (1.5 ** attempt),
max_profitable_fee
)
result = await send_transaction(tx, priority_fee)
if result.confirmed:
return result
# If all attempts fail, trigger protocol-level circuit breaker
await trigger_emergency_pause(reason="keeper_priced_out")
If liquidations can't execute for N consecutive slots, the protocol should automatically:
pub fn check_liquidation_health(ctx: Context<CrankCheck>) -> Result<()> {
let state = &mut ctx.accounts.protocol_state;
let current_slot = Clock::get()?.slot;
if current_slot - state.last_successful_liquidation_slot > STALE_THRESHOLD {
state.emergency_mode = true;
state.borrow_paused = true;
state.liquidation_bonus_bps += EMERGENCY_BONUS_INCREMENT;
emit!(EmergencyModeActivated {
slot: current_slot,
stale_slots: current_slot - state.last_successful_liquidation_slot,
});
}
Ok(())
}
Q1 2026 has already seen ~$137M in DeFi exploits. The Step Finance incident ($27M via compromised keys), the Resolv stablecoin exploit ($25M via unbacked minting), and the CrossCurve bridge drain ($3M) all targeted different vectors — but the Noisy Neighbor attack combines economic incentive alignment with low barrier to entry in a way none of these did.
The attacker doesn't need:
They just need:
This is not theoretical. The write-lock dependency graphs of the top 20 Solana DeFi protocols are public. Every account a program touches is visible on-chain. The only question is when — not if — we see this exploited at scale.
If you checked ❌ on 3+ items, your protocol is a Noisy Neighbor target today.
This article is part of the DreamWork Security research series on emerging blockchain attack vectors. Follow for weekly deep dives into DeFi security, audit tooling, and vulnerability analysis.
2026-03-26 12:41:32
we moved to building a Number Guessing Game. The system generates a random number, and the player tries to guess it with hints like “higher” or “lower.” Difficulty levels change the range of numbers, such as 1–20 for easy, 1–50 for medium, and 1–100 for hard.
Another important concept was randomness. Computers do not generate truly random numbers; they use pseudo-random algorithms based on a seed value. If the seed is the same, the generated sequence will also be the same. Usually, system time is used as the seed to make it appear random.
We also handled user input carefully by converting it into the correct type and validating it to avoid errors. Then we introduced a leaderboard system to store and display player performance.
Below is the implementation of the Number Guessing Game with a simple leaderboard and sorting:
import random
leaderboard = []
def get_difficulty():
print("1. Easy (1-20)")
print("2. Medium (1-50)")
print("3. Hard (1-100)")
while True:
try:
choice = int(input("Choose difficulty: "))
if choice == 1:
return "easy", 20, 10
elif choice == 2:
return "medium", 50, 8
elif choice == 3:
return "hard", 100, 5
else:
print("Invalid choice")
except:
print("Enter a valid number")
def play_game():
name = input("Enter your name: ")
difficulty, max_range, attempts = get_difficulty()
secret_number = random.randint(1, max_range)
print(f"\nGuess number between 1 and {max_range}")
for attempt in range(1, attempts + 1):
try:
guess = int(input(f"Attempt {attempt}: "))
except:
print("Invalid input")
continue
if guess == secret_number:
print("Correct!")
leaderboard.append({
"name": name,
"difficulty": difficulty,
"attempts": attempt
})
return
elif guess < secret_number:
print("Higher")
else:
print("Lower")
print(f"Game Over! Number was {secret_number}")
def view_leaderboard():
if not leaderboard:
print("No data yet")
return
difficulty_order = {"easy": 1, "medium": 2, "hard": 3}
sorted_data = sorted(
leaderboard,
key=lambda x: (difficulty_order[x["difficulty"]], x["attempts"])
)
print("\nLeaderboard:")
for p in sorted_data:
print(p["name"], p["difficulty"], p["attempts"])
def main():
while True:
print("\n1. Play Game")
print("2. View Leaderboard")
print("3. Exit")
choice = input("Enter choice: ")
if choice == "1":
play_game()
elif choice == "2":
view_leaderboard()
elif choice == "3":
break
else:
print("Invalid choice")
main()
Finally, we touched on security. Communication between systems happens over HTTPS using encryption, ensuring that data cannot be intercepted by attackers.
Overall, this session showed how a simple game can help us understand important concepts like concurrency, locking, database design, randomness, and security. It made me realize that even small programs can build a strong foundation for understanding real-world systems.
2026-03-26 12:39:27
The original pitch for cloud computing was simple: stop buying servers, rent someone else's. For most workloads over the past fifteen years, that trade worked. But AI infrastructure has rewritten the economics, and enterprises are responding by doing something few predicted — they're moving compute closer to the data, not further away.
A recent DataBank survey found that 76% of enterprises plan geographic expansion of their AI infrastructure, while 53% are actively adding colocation to their deployment strategies. This isn't a minor adjustment. It's a structural shift in how organizations think about where AI workloads should run.
Running inference on a large language model in a hyperscaler region costs real money. Not "line item you can bury in OpEx" money — more like "the CFO is asking questions in the quarterly review" money. GPU instance pricing on AWS, Azure, and GCP has remained stubbornly high because demand outstrips supply, and the cloud providers know it.
The math gets worse when you factor in data gravity. Most enterprises generate data in dozens of locations — retail stores, manufacturing plants, regional offices, edge devices. Shipping all that data to us-east-1 for processing, then shipping results back, creates latency and egress costs that compound as AI adoption scales.
Colocation flips this equation. You place GPU-dense compute in facilities close to where data originates, connect to cloud services where they make sense (object storage, managed databases, identity), and keep the expensive part — inference and fine-tuning — on hardware you control or lease at predictable rates.
The industry is moving toward what Seeking Alpha describes as a "cloud-smart" strategy — using public cloud, private cloud, and edge computing based on the workload profile rather than defaulting to one deployment model for everything.
This makes sense when you break down what AI workloads actually need:
Training still belongs in the cloud for most organizations. You need massive, bursty GPU capacity for weeks or months, then nothing. Buying that hardware outright is a terrible investment unless you're running training continuously. Hyperscaler reserved instances or on-demand capacity work fine here.
Inference is the opposite profile. It's steady-state, latency-sensitive, and runs 24/7. The cost-per-token adds up fast at scale. Running inference on colocated or on-premises hardware — especially with purpose-built accelerators — can cut costs 40-60% compared to cloud GPU instances, depending on utilization rates.
Fine-tuning sits in the middle. You need GPU capacity for days, not months, and the data involved is often sensitive enough that you don't want it leaving your network. A colocated setup with good connectivity to your data sources handles this well.
Data sovereignty and residency requirements are accelerating the geographic distribution of AI infrastructure in ways that pure cloud strategies can't easily accommodate.
The EU's AI Act imposes requirements on where and how AI systems process data. Healthcare organizations in the US deal with HIPAA locality requirements. Financial services firms face data residency rules that vary by jurisdiction. When your AI model needs to process customer data from Germany, running inference in a Virginia data center creates compliance headaches that no amount of architectural cleverness fully solves.
Enterprises are responding by deploying AI infrastructure across multiple geographies — not because they want the operational complexity, but because regulators and customers demand it. The 76% planning geographic expansion aren't chasing some multicloud vision. They're meeting regulatory reality.
Hybrid edge-cloud architectures add another layer. Manufacturing plants running quality inspection models can't tolerate 200ms round-trip latency to a cloud region. Autonomous systems need inference at the point of action. Retail environments process customer interactions in real time.
These use cases demand on-site or near-site compute with cloud connectivity for model updates, monitoring, and periodic retraining. The architecture looks less like "cloud with edge caching" and more like "distributed compute with cloud coordination." The control plane lives in the cloud. The data plane runs where the data lives.
This is a harder architecture to build and operate than a cloud-native deployment. It requires teams who understand networking, hardware lifecycle management, and distributed systems — skills that many organizations let atrophy during the cloud migration years.
If you're an infrastructure leader planning AI capacity for the next 2-3 years, here's the framework I'd use:
Audit your inference costs first. Most organizations are surprised by how much they're spending on cloud GPU instances for inference once they aggregate across teams and projects. This number is your baseline for a hybrid business case.
Map data gravity. Where does your training data originate? Where do inference requests come from? Where do results need to arrive? If the answer to all three is "the same cloud region," stay in the cloud. If it's "twelve different locations across three countries," you need a distributed strategy.
Don't build a GPU data center. Colocation with GPU leasing gives you the economics of owned hardware without the capital expenditure and refresh cycles. Companies like DataBank, Equinix, and CoreWeave are building exactly this model — dense GPU compute in colocation facilities with direct cloud interconnects.
Plan for heterogeneous accelerators. NVIDIA's dominance in training is real, but inference has viable alternatives — AMD Instinct, Intel Gaudi, AWS Inferentia, Google TPUs. A hybrid strategy lets you match accelerators to workload profiles instead of paying the NVIDIA tax on everything.
Invest in platform engineering. Hybrid AI infrastructure without a solid platform layer becomes an operational nightmare. You need consistent deployment pipelines, observability, and model lifecycle management that works across cloud regions, colo facilities, and edge locations. Kubernetes helps here, but it's the starting point, not the whole answer.
Going hybrid is operationally harder than going all-in on a single cloud provider. Anyone who tells you otherwise is selling colocation space. You'll manage more vendor relationships, more network paths, more failure modes.
But the economics and the regulatory environment have shifted enough that "just put it all in AWS" is no longer a defensible strategy for AI-heavy workloads. The organizations figuring out hybrid now — while GPU supply is still constrained and cloud pricing remains elevated — will have a meaningful cost advantage over those who wait.
The cloud isn't going away. It's just no longer the default answer for every AI workload. And the sooner infrastructure teams internalize that distinction, the better positioned they'll be when AI spending goes from "experimental budget" to "largest line item on the infrastructure bill."
2026-03-26 12:36:15
When distributing Windows applications via installers, it is standard practice to code sign the binaries before distribution. Code signing proves that the binary has not been tampered with and verifies the identity of the publisher.
With that in mind, I investigated what kind of architecture is needed to code sign Windows apps within a CI/CD pipeline on GitHub Actions.
The overall architecture of the system built on GitHub Actions looks roughly like the diagram below.
When integrating a code signing process into a CI/CD pipeline on GitHub Actions, the code signing private key must be stored in a cloud-based HSM (Hardware Security Module) that is accessible from the Windows machine running on GitHub Actions.1
There are two types of cloud-based HSMs: those provided by a Certificate Authority (CA), and those running in your own cloud environment. With the former, you may be charged based on the annual number of code signing operations, and those costs can be quite high. So, if managing your own infrastructure is not a burden, using your own cloud HSM is the recommended approach.
It is common practice to attach a timestamp when code signing. Without a timestamp, the date and time of signing is self-reported by the signer, meaning third parties cannot verify whether the certificate was valid at the time of signing (since one could set the machine's clock to a past date and sign). As a result, once the certificate expires, the code signature is also considered invalid.
In contrast, if a trusted third-party timestamp is attached, the signing date and time become verifiable, allowing third parties to confirm that the certificate had not been revoked at the time of signing. This means the code signature remains valid even after the certificate itself expires.
The code signing certificate issued by the CA does not contain sensitive data such as private keys. Therefore, it can be placed on the GitHub Actions Runner as a file.
First and foremost, you need to have a code signing certificate issued.
Here are the key points for this process:
If you generate the private key on your own cloud HSM, the general steps are as follows:
Step 3, creating the CSR, is typically done on a local Linux machine using an OpenSSL command like the following.
For information on how to use OpenSSL with cloud HSMs, please refer to What is the OpenSSL Engine API that enables integration between OpenSSL and cloud HSMs or YubiKey.
export PKCS11_MODULE_PATH=/tmp/libkmsp11-1.6-linux-amd64-fips/libkmsp11.so
export KMS_PKCS11_CONFIG=/tmp/pkcs11-config.yaml
openssl req -new -subj '/CN=example.com/' -sha256 \
-key pub.pem -engine pkcs11 -keyform engine \
-key pkcs11:object=sign_key_name > cert-request.csr
If generating the private key and creating the CSR yourself seems too cumbersome, using an HSM provided by the CA may allow some of these steps to be semi-automated.
Once the certificate has been issued, you need to set up the code signing environment on the Windows machine running on the GitHub Actions Runner.
Here are the key points for setting up the environment:
Once the environment is set up, all that remains is to call SignTool.exe to perform the code signing.
The following is an example command for signing with a Sectigo certificate:
signtool.exe sign /v /fd sha256 /t http://timestamp.sectigo.com /f path/to/mysigncscertificate.crt /csp "Google Cloud KMS Provider" /kc projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME/cryptoKeyVersions/1 path/to/file-tobe-signed.exe
Prior to 2023, it was possible to store private keys in PEM files. However, due to ongoing private key leakage incidents — exemplified by the NVIDIA code signing private key leak — the industry now requires that private keys be stored on an HSM when issuing code signing certificates. ↩
You can view the list of software pre-installed on Runners at https://github.com/actions/runner-images. ↩