nonce-watchtower
A read-only monitor for the standing powers attached to a set of Solana keys: the things that let someone move funds or seize control later, without asking the key holder again.
Given wallets, multisig-member keys, or a Squads v4 multisig address (it reads the members itself), it lists:
- Durable-nonce accounts whose authority is one of those keys. Pre-signed transactions that use them never expire until the nonce is advanced. For each one it looks up who created it (first transaction, funder, initial nonce authority, fee payer), and flags a nonce funded or first authorized by a key outside the watched set.
- SPL Token and Token-2022 delegate approvals on their token accounts (plus foreign close authorities, frozen accounts, and Token-2022 permanent delegates on mints they hold).
- Mint/freeze authorities of the mints you name, and upgrade authorities (plus last-deploy slot) of the programs you name.
watch mode polls on an interval; stream mode subscribes to Solana PubSub (WebSocket) and
re-scans within seconds of a change, with a full re-sync after every reconnect. Both keep a
local state file and alert when any of these change: stdout always, plus Telegram,
Discord and/or a generic JSON webhook.
It never signs or sends transactions and holds no private keys. It only needs public keys.
Threat model
The attack this is built around
On 1 April 2026 Drift Protocol lost roughly $270-285M. The attacker never broke Drift's contracts. Instead they:
- created durable-nonce accounts tied to Security Council members (23-30 March),
- got two of the five signers of the 2-of-5 Squads multisig to approve transactions whose real effect was misrepresented, and those signatures were bound to the nonces so they never expired, and
- about nine days later, submitted the pre-signed transactions, took admin control, and drained the protocol in minutes.
Sources: CoinDesk, "How a Solana feature designed for convenience let an attacker drain $270 million from Drift", Halborn, "Explained: The Drift Hack (April 2026)", BlockSec incident analysis.
The nonce accounts sat on-chain, in public, for about nine days before they were used. Any signer could have seen them with one query. This tool runs that query on a schedule, along with the related standing-approval checks.
What it detects
| Signal | Why it matters | Alert |
|---|---|---|
| New nonce account with a watched key as authority | Staging step for deferred pre-signed transactions | new_nonce_account (critical) |
| Nonce account funded (rent paid) by, or first initialized with an authority of, a key outside the watched set | Someone else staged a nonce on your key: the Drift pattern | nonce_outside_creator (high) |
| Nonce funded by a watched key, but the transaction fee was paid by an outside key | Sponsored/relayed fee: often benign, worth confirming | nonce_outside_fee_payer (medium) |
| Nonce creator could not be established (no history, or only the initialize step visible so the funder is unknown) | Unknown is not cleared | provenance_unavailable (warn) |
| Squads member added/removed, threshold or config authority changed | Who can approve changed | multisig_member_added / _removed / _threshold_changed / _config_authority_changed (critical) |
| Squads member permissions or time lock changed | Approval rules changed | multisig_permissions_changed, multisig_time_lock_changed (high) |
| Squads multisig has a config authority | That key can change members and threshold without a vote | squads_config_authority (medium) |
| Squads address is not a v4 multisig (wrong owner, v3, missing, undecodable) | Members are NOT being watched | squads_<status> (warn) |
| Nonce value changed | A durable-nonce transaction executed, or someone advanced the nonce | nonce_advanced (high) |
| New or changed token delegate, allowance increase | The delegate can move the tokens with no further signature | new_delegate, delegate_changed, delegate_allowance_increased (high) |
| Allowance decreased | The delegate may have spent the tokens | delegate_allowance_decreased (medium) |
| Foreign close authority, frozen account, permanent delegate | Third-party control over the account or its balance | medium |
| Mint/freeze/permanent-delegate/transfer-hook authority changed on a watched mint | Supply or freeze control moved | critical |
| Upgrade authority changed / program redeployed | Code-control takeover, or new bytecode | critical / high |
| A check stopped working | Changes in that area are no longer detected | coverage_lost (warn) |
stream: live connection down, or a subscription rejected |
Only periodic polling is running; changes are seen late | coverage_lost on stream / stream:<subscription> (warn) |
stream: reconnected, full re-sync scan complete |
States the outage window; net changes inside it are alerted normally | stream_gap (warn) |
stream: reconnected but the re-sync scan was incomplete |
The outage window is NOT verified yet | stream_gap_unresolved (warn) |
stream: a nonce account with a watched authority appeared live but is gone by the follow-up scan |
Created and closed (or re-authorized away) quickly, or the RPC lags | nonce_seen_live (critical) |
| An alert could not be delivered to Telegram/Discord/webhook | Alerts are queued and retried; other sinks are told | alert_delivery_failed (warn) |
| A sink's retry queue overflowed (more than 500 undelivered alerts) | Alerts were dropped: oldest non-critical first; the record of it is never dropped | alert_queue_overflow (high, critical if a critical alert was dropped) |
| The scan/alert/state cycle itself failed (bug, disk full, unwritable state) | Nothing is being detected or recorded until a cycle succeeds | scan_failed (high), sent straight to every sink; stream coverage scan_failed |
stream: a nonce notification could not be decoded and the next scan does not explain it |
A nonce created and closed in between would be invisible | nonce_notification_undecodable (high) |
| A known nonce account is missing from a scan, but a direct read shows it still exists | The RPC's answer was stale or partial | that wallet's nonce check becomes unverified (coverage_lost), the account is kept |
On the first run, every standing nonce and delegate is reported as existing_*, so a
nonce staged before you installed the tool does not get quietly accepted as baseline.
Security design
-
Read-only by construction. The RPC client only accepts an allow-list of methods (
getProgramAccounts,getTokenAccountsByOwner,getAccountInfo,getMultipleAccounts,getSlot, and for nonce provenancegetSignaturesForAddress,getTransaction). Any other method raises before any network I/O. The PubSub client has its own allow-list (accountSubscribe,programSubscribe,logsSubscribeand their unsubscribes). The package has no signing code, and a test checks that. -
Unknown is never reported as clean. If a check cannot run, it is reported as
unavailable,unverifiedorpartial,scanexits non-zero (bit 2), andwatchkeeps the last known values and emitscoverage_lost. An outage never looks like "the nonce account disappeared". -
Silent-filter canary. An RPC that quietly returns
[]for System-programgetProgramAccountswould make every wallet look clean. Each scan first queries the bricked (currently 26) nonce accounts whose authority is the System program itself (no one can ever sign as it, so they cannot be withdrawn). If that returns nothing, nonce coverage is markedunavailable. -
The server-side filter is not trusted. Every returned nonce account is decoded locally, and it is dropped unless its owner is the System program and its authority bytes equal the queried key.
-
Secrets stay out of output. RPC/WebSocket/webhook URLs and the Telegram bot token are credentials. They are read from environment variables. The config file refuses inline
rpc_url,ws_url,webhook_url,telegram_bot_tokenanddiscord_webhook_url. Every message shows onlyscheme://hostor the sink name and HTTP status. Exception text (which can echo a URL or token) goes through one central redactor (watchtower/redact.py) before it can reach stdout, stderr, the state file or any sink: every configured secret (bot token, webhook URLs and their path tokens, keyed RPC/WebSocket URLs: query strings, path API keys, userinfo) is masked, and any other URL is cut toscheme://host. Tests inject exceptions carrying each kind of secret through bothwatchandstreamand check every output channel. Residual: under the ambiguous keystoken=andauth=(outside a URL), a value shaped like an address or signature (32/64-byte base58) is left visible, because it is usually one; explicit credential keys (api_key,secret,password, ...) are always masked. Recognised forms:key=value,key: value, quoted values, JSON fields,Authorization: Bearer|Basic|Token <value>(header in any case), a bareBearer <value>(capital B only, so prose like "the bearer of" is untouched), and dict values under a credential key in the state file. Key names must match exactly: out of scope are unregistered secrets under other or compound key names (for examplekey=,sig=,password_hash,my_secret), a bare lowercasebearer <value>, unquoted values containing spaces, and multi-line forms (YAML blocks, XML); registered secrets (your configured URLs and tokens) are masked wherever they appear. Webhooks must be https; PubSub must bewss://(plainws://only for localhost). -
Answer age is bounded and checked. Every state read (
getProgramAccounts,getAccountInfo,getMultipleAccounts,getTokenAccountsByOwner) asks forfinalizedcommitment with its context slot, and immediately before EACH read the tool takes a fresh reference from the same endpoint. What that proves:- Internal consistency: the answer's context slot is at most
max_slot_lagslots behind the endpoint's own finalized slot (default 64, about 25 s;max_slot_lag/--max-slot-lag). This catches an index lagging its node; the canary alone cannot, because old canary accounts are still listed by an index that has not caught up with a nonce created seconds ago. - Absolute age:
getBlockTimeof that finalized slot is at mostmax_block_ageseconds before this machine's clock (default 120;max_block_age/--max-block-age). This catches a node that is uniformly behind the chain, whose slot and index agree with each other but are both old. It relies on this machine's clock being right (run NTP). - Optional independent reference: with
WATCHTOWER_REFERENCE_RPC_URLset (name configurable viareference_rpc_url_env; a credential, never printed), the primary's finalized slot may trail that second RPC's by at mostmax_slot_lag.
Failing any of these, or an answer without a context slot, or a failed
getSlot/getBlockTime, makes that checkunverified(nonces) orunavailable: a coverage gap, never clean. What it does not prove: an endpoint that fabricates consistent, current-looking slots and block times while omitting accounts passes 1 and 2; only an honest, independent reference (3) narrows that. Inside the bounds an answer can still trail the chain by up tomax_slot_lagslots. Raise the limits if your provider load-balances across nodes that drift. Provenance history reads (getSignaturesForAddress,getTransaction) are not age-checked. - Internal consistency: the answer's context slot is at most
-
Squads members are read, not trusted blindly. The multisig account must be owned by the Squads v4 program, carry the v4
Multisigdiscriminator, decode cleanly (permission masks < 8, no duplicate members), and its decodedcreate_key+bumpmust derive its own address. Any failure is reported (wrong_owner,unsupported_v3,missing,undecodable,unavailable), the scan is incomplete (exit bit 2), and no member list is guessed. Inwatchmode, the last verified member list stays watched while resolution is failing. -
Provenance is decoded locally. The creating System-program instruction (
CreateAccount,CreateAccountWithSeedorInitializeNonceAccount, top level or CPI, including v0 lookup-table addresses) is decoded from raw instruction data, not from the RPC's parsed view. -
State file is written atomically with mode 0600.
-
Strict config. Unknown keys are rejected. This includes the TOML trap of writing
mints = [...]after a[[wallets]]header, which would otherwise silently drop the list. -
No third-party dependencies. Python 3.11+ stdlib only, so there is no supply chain beyond CPython. That includes the WebSocket client (
watchtower/ws.py, a minimal RFC 6455 client: handshake withSec-WebSocket-Acceptcheck, masked frames, ping/pong, close, fragmentation, size cap, no extensions).
Not in scope / limits
- It sees state, not intent. Every nonce is still flagged for a human to confirm, even one your own key created. Provenance answers only "which key funded the account, which authority it was first initialized with, and who paid the fee". The funder and initial authority decide "outside"; an outside fee payer alone is reported separately at medium. "Created by watched key X" does not prove X's holder meant to: a stolen or tricked member key looks the same. "Created by an outside key" is a strong signal, not proof of an attack (a wallet app or relayer can legitimately pay fees).
- Provenance limits. It needs an RPC that serves full transaction history for the nonce
account. Many providers and self-hosted nodes prune old ledger data; then the oldest visible
transaction is not the creation, and the answer is
unverified/unavailable, never a guess. If only theInitializeNonceAccountstep is visible (theCreateAccountwas in an earlier transaction that is not visible), the funder is unknown and the answer isunverified. Histories longer than 10,000 signatures (a busy nonce) are not walked and are reportedunavailable. Only the 3 oldest successful transactions are inspected. If an account was closed and re-created at the same address (this needs the address keypair), the first creation is reported. A settled provenance is cached in thewatchstate file and not re-fetched, and a later failed lookup never overwrites it. - It does not stop signing. It complements pre-sign decoders such as sign-safe and wallet-side nonce warnings. It is the after-the-fact, always-on layer.
- Polling latency (
watch). Default interval is 300 s. An attacker who stages a nonce and uses it within one interval is caught only after the fact (nonce_advanced/nonce_account_gone). Usestream(or a shorter interval) for council keys. - Streaming limits (
stream). Subscriptions usefinalizedcommitment (the same view the scan reads), so an alert lands roughly 15-30 s after the transaction, plus the scan time. Notifications are triggers, not evidence: each one (debounced, at most one scan per--min-gapseconds) runs the same full scan aswatch. A WebSocket can drop notifications without disconnecting, so a full re-sync scan also runs every--resync-intervalseconds. While the stream is down,streamkeeps polling at that interval. A change that happens and reverts inside an outage or between two scans is not visible; the outage itself is always reported (coverage_lostonstream, thenstream_gap). Some providers refuseprogramSubscribeon the System or Token programs; that subscription is reportedsubscribe_failedand its changes are seen only by polling. A watched mint's supply changes do not trigger scans (only its other bytes do). - RPC availability. Finding nonces by authority needs
getProgramAccountson the System program with amemcmpfilter. On 2026-10-06 the publicapi.mainnet-beta.solana.comserved it. Some providers block it (see "RPC" below), and the canary will catch that. - Mints and programs are checked only when you list them. The tool does not yet discover every mint or program a key controls (M2).
- Programs under loader-v4 are reported as
unsupported_loader. - Squads: only v4 (
SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf) is decoded. A v3 (SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu) multisig is reportedunsupported_v3: list its members yourself. Only vault index 0 is derived and watched; vaults 1..255 are not. Members are re-read every cycle, so a member change is seen within one polling interval, not instantly. Instreammode the multisig account is subscribed, so a member change triggers a scan within seconds. Spending limits, proposals and pending transactions are not inspected. SPL Governance (Realms) and other multisig programs are not supported. - A local attacker who can edit the state file can suppress alerts.
Related work
- solana-nonce-guard (Rust, MIT): audits a Squads/SPL multisig for durable-nonce staging, and has a WebSocket monitor. It covers nonces only.
- solgov (TypeScript, MIT): governance monitoring for a fixed list of about 50 protocols, including nonce detection on their signers.
- sign-safe (MIT): decodes a transaction before you sign it.
nonce-watchtower works on any set of keys, combines nonces, delegates and authorities in one diffing watcher, and is designed to fail loudly instead of reporting clean.
Install
python3 -m venv .venv
.venv/bin/pip install nonce-watchtower # or, from a checkout: .venv/bin/pip install -e .
.venv/bin/watchtower --help
Python 3.11+, no dependencies. The console script is watchtower (python -m watchtower works too).
Usage
# one-off scan (exit: 0 clean, 1 findings, 2 incomplete coverage, 3 both, 64 usage)
watchtower scan <pubkey> [<pubkey> ...] [--mint MINT]... [--program PROGRAM_ID]... [--json]
# watch every member, vault 0 and (if set) the config authority of a Squads v4 multisig
watchtower scan --squads <MULTISIG_ADDRESS> [--squads ...] [<pubkey> ...] [--no-provenance]
# polling watch (rescans every --interval seconds)
export WATCHTOWER_RPC_URL='https://your-provider.example/?api-key=...' # optional, recommended
export WATCHTOWER_WEBHOOK_URL='https://hooks.slack.com/services/...' # optional
watchtower watch --config wallets.toml --interval 300
watchtower watch --config wallets.toml --once # one cycle, for cron/systemd timers
# live: WebSocket subscriptions, full re-sync every 300 s and after every reconnect
export WATCHTOWER_WS_URL='wss://your-provider.example/?api-key=...' # optional: derived from the RPC URL
watchtower stream --config wallets.toml [--resync-interval 300] [--min-gap 10] [--json]
# check that alerts actually arrive (sends one test message to every configured sink)
watchtower alert-test --config wallets.toml
watch --once exits 0 when the cycle ran and every alert was delivered, and 2 when the cycle
failed, a configured alert sink did not accept its alerts, or alerts are still queued behind the
per-cycle message cap (stderr says ALERTS NOT YET DELIVERED; the next run sends them). Either
way a timer unit shows as failed.
See examples/wallets.example.toml. Top-level squads/mints/programs must come before the
first [[wallets]] block. squads = ["<multisig address>"] adds every member key, vault 0 and
any config authority to the watched set. The multisig account address is the one Squads shows in
its app URL, not the vault address.
--squads takes the multisig account, not the vault. Members found this way are labelled
squads <addr>… member N (permissions), vault 0 or config authority.
Nonce provenance runs by default. Each nonce account costs at least two extra RPC calls on the
first scan (--no-provenance skips it in scan; the report then says provenance: skipped).
Webhook payloads carry text (Slack), content (Discord) and a machine-readable alerts
array. If a delivery fails, the alerts are queued in the state file and retried next cycle.
Alerts: Telegram and Discord
Set the secrets as environment variables (names can be changed in wallets.toml, see
examples/watchtower.env.example):
| Sink | Variables |
|---|---|
| Telegram | WATCHTOWER_TELEGRAM_BOT_TOKEN (from @BotFather) and WATCHTOWER_TELEGRAM_CHAT_ID (or telegram_chat_id = ... in the config; a chat id is not a secret). Optional WATCHTOWER_TELEGRAM_THREAD_ID: a forum topic id (positive integer), sent as message_thread_id with every message, including scan_failed and alert-test |
| Discord | WATCHTOWER_DISCORD_WEBHOOK_URL (https://discord.com/api/webhooks/<id>/<token>) |
| Generic webhook | WATCHTOWER_WEBHOOK_URL or --webhook |
Then run watchtower alert-test --config wallets.toml. Delivery rules:
- Fail loud at startup. A malformed token, a non-Discord or non-https webhook URL, or Telegram with only one of token/chat id set is a startup error (exit 64), never a silently disabled sink.
- Retry, then queue. Network errors, HTTP 429 (honouring
retry_after) and 5xx are retried 3 times per message with backoff. A 4xx (bad token, bot not in the chat, deleted webhook) is not retried. Undelivered alerts are kept per sink in the state file (up to 500) and retried every cycle; a sink that fails never blocks the others. - Surface failures. A failed delivery is printed to stderr (
ALERT DELIVERY FAILED), sent asalert_delivery_failedthrough the sinks that still work, and makeswatch --onceexit 2. - Overflow is never silent. A queue holds at most 500 alerts. Beyond that the oldest
non-critical alerts are dropped first (critical ones only if nothing else is left), and one
alert_queue_overflowalert (count, oldest time, kinds) takes their place at the front of the queue. It is never trimmed itself, is reported on stderr asDROPPED, and makeswatch --onceexit 2. The dropped alerts are still in stdout/the journal. - A failed cycle is an alert, not a log line. If the scan, diff, delivery or state write
raises, a
scan_failedalert goes to stdout and straight to every sink (bypassing the queue and state file, which may be what failed). A sink that misses it is retried on every later attempt. Instreammode, retries back off after consecutive failures (--min-gap, doubling, capped at--resync-interval, reset by the first fully successful cycle), and a freshscan_failed("STILL FAILING", with the gap's start time) is sent so that delivered ones are never more than one--resync-intervalapart;watchraises one on every failed cycle. Instreammode thestreamcheck readsscan_failedand a gap is open until a cycle fully succeeds; that cycle then deliverscoverage_lostandstream_gapthrough the normal, persisted path. - Deduplicate only unchanged repeats. A non-critical alert is not sent again to a sink when
it is identical (apart from its timestamp) to the latest alert that sink saw for the same
subject, within
alert_dedup_seconds(default 1800). This absorbs exact repeats such as a recurringalert_delivery_failednotice. It never absorbs a change: if anything else was alerted on that subject in between (A -> B -> A, for example a member added, removed and re-added, or a check lost, restored and lost again), the repeat is sent, at every severity. Critical alerts are never deduplicated. stdout always gets every alert. Delivery is at-least-once: a crash between sending and saving the state can repeat a message. - Nothing is cut. Long batches are split across messages (Telegram 4096, Discord 2000
characters). At most 10 HTTP sends per sink per cycle, counting every piece of a split line;
alerts are held whole for the next cycle rather than half-sent. The one exception: a single
alert so long that it alone needs more than 10 messages is sent whole when it is first in line,
since it could otherwise never be delivered.
Messages are plain text, and Discord mentions are disabled so on-chain strings cannot ping
@everyone.
Streaming mode
watchtower stream opens one PubSub connection and subscribes, at finalized commitment, to:
| Subscription | Catches |
|---|---|
programSubscribe System program, dataSize 80 + authority memcmp, per watched key |
A nonce account created for, or re-authorized to, a watched key. The authority is instruction data, not an account key, so a logsSubscribe mention filter would miss a nonce staged by an outside key. |
programSubscribe SPL Token (dataSize 165) and Token-2022, owner memcmp, per watched key |
Delegate approvals, allowance changes, close authorities, freezes |
accountSubscribe each known nonce account |
Nonce advanced, closed, or authority moved away |
accountSubscribe each Squads multisig, watched mint, program and its ProgramData |
Member/threshold changes, authority changes, upgrades |
Watched keys include every Squads member, vault 0 and config authority. New nonce accounts and new members are subscribed after the scan that finds them.
Order of operations on every (re)connect: subscribe, wait for every subscription to be confirmed or rejected, then run a full re-sync scan, so a change between the scan and the subscription cannot fall through. Reconnects use exponential backoff with jitter (1 s doubling to 60 s, reset after a connection stayed up 60 s). A connection that delivers no frame, not even a pong to our 30 s ping, for 90 s is treated as dead.
After each scan the subscription set is brought in line with what was found: new nonce
accounts and members are subscribed, and targets that left (a closed nonce account, a removed
member) are unsubscribed. A nonce account only "leaves" once a direct getAccountInfo confirms it
is closed or no longer has a watched authority. If the scan omitted it but it still exists (a
stale or partial RPC index), it stays watched and subscribed and the wallet's nonce check is
unverified, so the gap stays open. Every nonce notification is decoded locally. One that cannot
be decoded is reported (nonce_notification_undecodable) unless the next scan read that account
itself.
A coverage gap is never reported as clean. The stream check is ok, partial (some
subscriptions rejected) or disconnected. stream: ok means the connection is alive and
the subscriptions were accepted; it does not prove notifications are flowing. A connection
that stays up (answers pings) but silently stops forwarding notifications looks healthy, and
such a change is caught only by the next full re-sync scan. Freshness is therefore bounded by
--resync-interval (default 300 s), not by the stream.
Structural limit: coverage is continuous only to the extent the provider delivers
notifications. A change AND its revert that both land between two full scans, with both
notifications dropped by the provider while the socket stays alive, cannot be detected: each
scan sees the same state, and no notification arrived to say otherwise. nonce-watchtower never
claims complete continuous coverage. It claims: every full scan is checked for completeness and
freshness, every outage it can observe is reported, and nothing unverified is reported as clean. After reconnecting, stream_gap gives the window
(before = last frame received, after = re-sync time). It is raised only when the re-sync
scan ran every check. Otherwise stream_gap_unresolved is raised and the window stays open until
a complete scan. A restart of the process is reported the same way, from the state file's last
update.
Running as a service
examples/nonce-watchtower-stream.service runs stream under systemd with a throwaway
DynamicUser, state in /var/lib/nonce-watchtower, secrets in a 0600 EnvironmentFile
(examples/watchtower.env.example), Restart=always, and a strict sandbox (read-only
filesystem, no new privileges, IP sockets only). Install steps are in the file's header. Logs go
to the journal (journalctl -u nonce-watchtower-stream -f).
RPC
The default is https://api.mainnet-beta.solana.com. If an endpoint refuses or filters
System-program getProgramAccounts, the scan says so. It prints advice to set
WATCHTOWER_RPC_URL to a provider or your own node that serves it, and it marks nonce
coverage UNKNOWN. Passing --rpc with a key in it works, but it leaves the key in your
shell history.
Development
.venv/bin/pip install 'setuptools>=77' # only for the clean-install test
.venv/bin/python -m unittest discover -s tests -t .
Tests use JSON fixtures recorded from mainnet (see tests/fixtures/README.md) and never
touch the network. The WebSocket, Telegram and Discord endpoints are replaced by in-memory
fakes, and streaming tests run on a fake clock. tests/test_packaging.py builds the sdist,
builds the wheel from the unpacked sdist, installs it into a fresh venv with --no-index, and
runs watchtower --help plus an offline smoke test from outside the source tree. It is skipped
(and says so) if setuptools>=77 is not installed.
Verified layouts
-
Squads v4
Multisig(Anchor/Borsh, decoded sequentially): 8-byte discriminatorsha256("account:Multisig")[:8],create_key,config_authority(all-zero = autonomous),thresholdu16,time_locku32,transaction_indexu64,stale_transaction_indexu64,rent_collectorOption<Pubkey>(1 tag byte, payload only when Some),bumpu8,membersVec<(Pubkey, permissions u8)>(Initiate 1, Vote 2, Execute 4). Multisig PDA seeds[b"multisig", b"multisig", create_key]; vault PDA seeds[b"multisig", multisig, b"vault", u8 index]. Source: Squads-Protocol/v4 at commitaf94153f,state/multisig.rs,state/seeds.rs,instructions/multisig_create.rs,sdk/multisig/src/pda.ts. Checked against mainnet: the decoded Exponent Finance multisig derives its own address, and its vault 0 equals the on-chain upgrade authority of the Exponent program; the Manifest vault published in its README (bump 253) is reproduced. -
PDA derivation:
sha256(seeds || bump || program_id || "ProgramDerivedAddress"), rejected when the result decompresses to an ed25519 point (same rule as curve25519-dalek). Checked against real associated-token-account addresses. -
System instructions used for provenance (bincode u32 tag):
CreateAccount= 0 (accounts: funder, new),CreateAccountWithSeed= 3,InitializeNonceAccount= 6 (accounts: nonce, …; data: authority). -
Nonce account (80 bytes, bincode):
Versionsu32 tag @0,Stateu32 tag @4, authority @8, durable nonce @40, lamports_per_signature @72. Seeanza-xyz/solana-sdknonce/src/state.rs(State::size() == 80) andversions.rs. -
Upgradeable loader:
Program{tag=2, programdata@4},ProgramData{tag=3, slot@4, Option<authority> tag@12, authority@13}.
License
MIT
Metadata
Release files for nonce-watchtower 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nonce_watchtower-0.3.0.tar.gz | 153.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nonce_watchtower-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 231.8 kB
Release files / nonce_watchtower-0.3.0.tar.gz
| Download URL | nonce_watchtower-0.3.0.tar.gz |
|---|---|
| Size | 153.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b1b7463e86888a29dc0f9880f4bde802ae6610b5e3f0c5db534be47dcf0902a2
|
|
BLAKE2b-256 checksum How to use checksums |
e0dc6eaf9683a18dffc84aa3cc0fd19a0d8eb973986cd8b4034464fcba33d69d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / nonce_watchtower-0.3.0-py3-none-any.whl
| Download URL | nonce_watchtower-0.3.0-py3-none-any.whl |
|---|---|
| Size | 78.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
787b90a2284a4d4eca47e9a8d68e073fd1f0540cab9c4e1492b359921f6c1e46
|
|
BLAKE2b-256 checksum How to use checksums |
630504a0f33c491c62088586be24e3ebf955d7fb125839dd5c97789a6bd66562
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log