Skip to content
ConcatenaLabsPublic

About

OpenAMP: open-source restricted-asset issuance and transfer-approval service for Sequentia (an AMP2 equivalent) with opt-in confidentiality. Live API at sequentiatestnet.com/openamp/v1/

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

113 Commits

Folders and files

Repository files navigation

OpenAMP

Open-source issuer-governed assets for the Sequentia network: a self-hostable equivalent of Blockstream's AMP2. Issuers manage regulated assets (securities, funds, bonds) whose every transfer is co-signed by an issuer-controlled policy server, with registration/KYC whitelisting, categories, velocity limits, holder caps, lock-in and vesting, freezing, clawback under disclosed terms, ownership reports, and a hash-chained transparency log that can be anchored on-chain. It requires zero consensus changes: the daemon (openampd, written in Go) talks to an ordinary Sequentia node over JSON-RPC, and all enforcement lives in taproot script plus the policy server's signature.

This is testnet software. Everything here runs against the Sequentia public testnet (parent chain: Bitcoin testnet4); there is no mainnet.

Design document: DESIGN.md. Frozen format specifications live under spec/.

The live instance, and what it serves

  • openampd runs on the Sequentia public testnet behind https://sequentiatestnet.com/openamp/ (REST API only, no web UI). It governs the demo restricted asset BONDX:

    curl -s https://sequentiatestnet.com/openamp/v1/assets
    

    The same instance also governs two network-enforced (OpenDAMP) assets, NEPLT and NEPL2 ("enforcement": "damp" in their contracts).

  • The daemon implements: registration, enclave addresses and balances, hosted transfers with fee conversion or sponsorship, raw co-signing of self-built transactions, hosted issuance (demo mode) with optional blinded minting, per-transfer opt-in confidentiality end to end (blinded or explicit transfers of any asset, mixed explicit+blinded enclave sets, watch-wallet unblinding), freezes, categories, per-asset rules (velocity, holder cap, lock-in, vesting), clawback, ownership reports, transparency log with on-chain anchoring, and a reorg-aware chain follower.

  • Also running on the live testnet: the FROST threshold backend for the policy key (-signer frost), with distributed key generation and a transport seam for running the quorum across hosts. Its members are separate roles in one process today, which is a deployment posture rather than a protocol limitation — see "Trust model" below.

Trust model

  • Enclave outputs. A restricted asset exists only in taproot outputs with a NUMS internal key (no key-path spend) and script leaves <K_user> CHECKSIGVERIFY <K_policy> CHECKSIG (transfer) and, if enabled at issuance, <K_issuer> CHECKSIGVERIFY <K_policy> CHECKSIG (clawback). Every spend therefore needs the policy server's signature in addition to the holder's: enforcement is server-side, not consensus-side.
  • The asset ID commits to the policy key. The issuance contract JSON (which names K_policy) is hashed into the asset's issuance entropy, so anyone holding the contract can verify offline that a given asset ID is governed by a given policy key. No registry state, no consensus lookup. Format: spec/contract-v1.md.
  • Policy key backend. On-chain, K_policy is always a single x-only key, so the signing backend is swappable without any chain migration. Two backends ship, selected with -signer: local (default; one software key per asset in a 0600 file) and frost (a 2-of-3 threshold Schnorr quorum whose group public key IS K_policy, so no address, asset id, or wallet changes when you switch). Under frost, no single share can sign, every partial is verified before aggregation, and assets issued under local keep working through a per-asset fallback. Keys are generated by a DISTRIBUTED key generation (Feldman VSS with a per-member proof of knowledge): every member samples its own polynomial and verifies the shares it is dealt against the sender's published commitments, so no component — dealer, coordinator or member — ever holds the group secret, and a member that misbehaves is named while the run aborts leaving no key material behind. The trusted-dealer split stays available via OPENAMPD_FROST_KEYGEN=dealer, which is how the pinned test vectors are reproduced. The quorum runs in process today — the members are separate roles sharing one host and one 0600 keys file, so the threshold protects against a signing-path bug rather than host compromise; moving members onto their own hosts means implementing the Member/Transport seam in openampd/internal/server/frostsigner/transport.go and touches no protocol code.
  • Confidentiality is opt-in per transfer. Sequentia is transparent by default with opt-in confidentiality, and OpenAMP follows: an asset is never "a confidential asset" — any holder may receive or move ANY restricted asset confidentially in a given transaction. POST /v1/transfers takes "confidential": true to blind that transfer's enclave outputs (amounts and asset tags hidden on-chain); GET /address?confidential=1 returns the blinded (blech32) enclave address; issuance accepts "confidential": true meaning only "blind this mint transaction". A later mint cannot be blinded: the supply cap counts it from the explicit issuance on its own input, so the amount is public whatever the output says. An enclave can hold a mix of explicit and blinded coins, and every read (balances, holders, supply) unifies both views. The policy server derives and holds the per-(holder, asset) blinding keys in a node watch wallet, so the issuer sees and reports every holding and outside observers see nothing: confidentiality is privacy from outsiders only, never from the issuer. No confidential-transaction crypto is reimplemented: the node's rawblindrawtransaction/unblind machinery does the work, and the holder still only signs the returned sighashes.
  • Supply caps. The server never holds a reissuance token. Every asset's token goes, in its issuance transaction, into a supply cap: a Simplicity covenant from sequentia-contracts (templates/supply_cap, docs/supply-cap.md) that mints only while the total minted through it stays at or under a cap fixed at issuance. This server's operator key signs mints; a cold key kept off this server can move the covenant to new keys. No path takes the token out or raises the cap, so a compromise of this server mints at most the room left under the cap, and a compromise of the cold key gains no more.
  • Fees (Rule 1). A restricted asset never appears in a fee output, so it can never be swept into a block producer's coinbase. The policy server refuses to co-sign such a transaction, and Sequentia's default-deny fee-asset whitelist makes it non-paying at every producer regardless. Holders still pay costs in the restricted asset via fee conversion (the issuer takes a fee-equivalent slice into its own enclave and attaches the real fee in an ordinary asset, atomically in the same transaction), or the server sponsors the fee, or the sender self-pays in an ordinary asset with no issuer involvement.
  • Trees. An issuer may also hold an asset's holdings in a tree of off-chain leaves run by an operator it names (POST /v1/issuer/tree), where a transfer between registered holders settles at once without a transaction. Each leaf is an enclave-1 leaf: its owner's key (derived from the holder's registered key, so only that holder signs for it) and the operator's key spend it together, and the owner alone, after a delay, can only take it home to the owner's own enclave, in full. The operator co-signs a leaf's spend only beside this server's signature over the same message, and this server signs only a movement that passes the asset's rules, exactly as on the chain; the scripts and messages it checks come from arca-enclave, a tool built from the arca repository, so they are the ones the tree uses. What the issuer can do to a coin in the tree, in plain words: refuse to co-sign its move (a freeze, or any rule), and, only where the asset elected clawback at issuance, take back a leaf once it is on the chain, through the same claw leaf the enclave carries. What it cannot do: move a holder's leaf without the holder's signature, or stop the holder taking it home. A tree transfer counts toward velocity like any other, and a holder's leaves count in every balance, holder count and ownership report.
  • Transparency log. Every registration, rule change, freeze, transfer approval, refusal, and clawback is appended to a hash-chained public log (GET /v1/log); clawbacks are logged before they are signed. The issuer can anchor the log head on-chain in an OP_RETURN (POST /v1/issuer/anchor).
  • Reorg awareness. Sequentia reorganizes whenever Bitcoin reorganizes (anchoring is supreme). The chain follower detects forks and re-marks transfer records above the fork point as unconfirmed, so velocity accounting and reports reflect only the surviving chain.

REST API

Base URL of the public testnet instance: https://sequentiatestnet.com/openamp (local default: http://127.0.0.1:8722). All bodies are JSON; unknown fields are rejected. Errors are {"error": "<message>"} with a meaningful HTTP status; policy refusals are HTTP 403 and are recorded in the transparency log.

Wallet surface (no authentication)

Method and path Purpose
POST /v1/users Register a pubkey set, returns the account ID (AID)
GET /v1/users/{aid} Registration status
GET /v1/users/{aid}/address?asset=<id> Enclave address and spend data for an asset (&confidential=1 for the blinded form)
GET /v1/users/{aid}/balance?asset=<id> Confirmed enclave balance
POST /v1/transfers Build a hosted transfer (fee conversion or sponsorship)
POST /v1/transfers/{id}/complete Submit the holder's signatures, co-sign and broadcast
POST /v1/cosign Co-sign a self-built transaction (fee self-paid), including a board into an asset's tree
POST /v1/tree/leaves/{salt}/exited Close a tree leaf its holder took home to its enclave (anyone may report; the chain is the proof)
GET /v1/assets All assets with their contracts
GET /v1/assets/{id} One asset
GET /v1/supply?asset=<id> Chain-derived circulating supply, the asset's supply cap and, for a network-enforced asset, its verifier-asset token's covenant: {"asset", "circulating_atoms", "height", "supply_cap", "verifier_token"}
GET /v1/snapshots?asset=<id>[&seq=<n>] Published policy snapshots (network-enforced assets; latest, or one by seq)
GET /v1/log The transparency log (JSON lines)

POST /v1/users, body {"pubkeys": ["<x-only hex>", ...]}, response {"aid": "<40-char hex>"}. The AID is sha256("openamp-aid-v1" || sorted pubkey hex) truncated to 20 bytes. Registering the same key set twice returns the same AID. pubkeys[0] is the active enclave key.

GET /v1/users/{aid}/address?asset=<id> returns everything a wallet needs to receive and later spend from its enclave. With &confidential=1 the primary address is the blinded (blech32) form instead; either way both forms are returned, and either way the server imports the enclave script and its blinding key into the watch wallet so later blinded receipts to this enclave stay readable:

{
  "aid": "...", "asset": "...",
  "address": "<taproot address; blech32 when confidential=1>",
  "address_confidential": "<blech32 confidential address>",
  "confidential": false,
  "script_pubkey": "<hex>",
  "user_pubkey": "<x-only hex>",
  "transfer_leaf": "<leaf script hex>", "transfer_control": "<control block hex>",
  "claw_leaf": "<hex, only if the asset has clawback>", "claw_control": "<hex>"
}

GET /v1/users/{aid}/balance?asset=<id> returns {"aid", "asset", "atoms", "utxos"} from a confirmed UTXO-set scan.

POST /v1/transfers, body:

{"asset": "<id>", "sender_aid": "...", "recipient_aid": "...",
 "atoms": 1000, "fee_mode": "convert", "confidential": false}

confidential (default false) is the per-transfer privacy choice: true blinds this transaction's enclave outputs to the server-derived blinding keys, whatever the asset; false builds the asset legs explicit even when some of the sender's coins are blinded (spending a blinded coin then requires one blinded balancing output, which the server puts on its own fee change). fee_mode is "convert" (a fee-equivalent slice of the asset, rules.fee_convert_atoms, goes to the issuer's enclave and the server attaches the real fee) or "sponsor" (the server pays the fee and takes nothing). Self-paid transactions use POST /v1/cosign instead. Response:

{"id": "<transfer id>", "tx": "<unsigned tx hex>",
 "to_sign": [{"input": 0, "sighash": "<32-byte hex>", "pubkey": "<x-only hex>"}],
 "convert_atoms": 100, "fee_sats": 1000, "fee_asset": "<display hex>"}

fee_sats is the fee in atoms of fee_asset, the asset the server's fee rule picked for this transaction.

The wallet signs each sighash with BIP340 Schnorr under its enclave key. Pending transfers, burns and clawbacks expire after 72 hours.

POST /v1/transfers/{id}/complete, body {"sigs": {"<input index>": "<64-byte schnorr sig hex>"}}. The server verifies the holder's signatures, runs the policy engine, attaches the policy signatures and its own fee-input signature, broadcasts, and returns {"txid": "..."}. A policy refusal returns 403 with the reason.

POST /v1/cosign for self-built transactions (the sender attaches its own ordinary-asset fee). Self-built flows are transparent-only: a transaction containing blinded outputs, or claiming a blinded enclave coin as an input, is refused whatever the asset — per-transfer confidentiality lives in the hosted flow. Body:

{"tx": "<tx hex>", "asset": "<id>", "sender_aid": "...", "inputs": [0, 1]}

inputs are the indices spending the sender's enclave outputs. The server checks that each claimed input really is the sender's enclave output, that no unclaimed input spends this asset's enclaves, and runs the same policy engine. Response (the caller assembles the witnesses and broadcasts):

{"sigs": [{"input": 0, "sighash": "...", "policy_sig": "...",
           "leaf": "...", "control": "..."}]}

Witness stack for an enclave input, bottom to top: <policy sig> <user sig> <leaf script> <control block>.

Policy engine. Both approval paths re-derive everything from the transaction and the chain (request metadata is never trusted) and refuse when: the sender or a non-issuer recipient is frozen; any output's asset or value is blinded; the restricted asset appears in a fee output; an output pays the asset to a non-enclave script (OP_RETURN burns allowed only if the contract permits them); a recipient lacks a required category; the asset is inside its lock-in window (issuer-bound conversion exempt if configured); the sender's velocity window would be exceeded; the holder cap would be exceeded; or the sender's vested balance is insufficient.

An asset's tree

Every tree call needs -arcaenclave (the tool) and answers 501 without it. The tool reads a board record and a transfer for this server: arca-enclave board and arca-enclave transfer, JSON on stdin and stdout. This server fills the asset's terms (its chain, the tree's operator, the policy key and the clawback election) into every request it sends the tool, whatever the caller sent.

POST /v1/issuer/tree {"asset", "operator": "<x-only key>"} enables the asset's tree and logs it. GET /v1/assets/{id} then carries "tree": {"operator", "policy", "clawback": "<issuer key>" | null}. The operator cannot change while any leaf of it is held.

Boarding. A holder brings coins into the tree by paying its enclave coin to a board output, through POST /v1/cosign with the board named: "tree_outputs": [{"output": 0, "record": "<board record hex>"}]. The output is admitted only when the tool reads the record as an enclave-1 board of this asset's tree (its policy key, its clawback election, its operator, this chain) for a registered holder, whose script and explicit value are exactly the output's, at a salt the register has never held. The holder is then that output's recipient for every rule. The leaf enters the register boarding, and is live once the board transaction is in a block (boarding again if a reorganisation takes the block away).

POST /v1/issuer/tree/transfer {"asset", "transfer": <the tool's transfer request: inputs (coin records with their checkpoint values) and outputs (holder key, value, nonces, exit delay)>}, called by the tree's operator. Refused unless every input is a live leaf of one sender at the value the tool reads, whose checkpoint keeps the whole coin; every output an enclave-1 leaf of a registered holder; and the outputs take every atom, since a remainder of a restricted asset could go anywhere. Then the same rules as on the chain (freeze, categories, category-deny windows, lock-in, velocity, holder caps, vesting, pledges), each refusal worded as on the chain, HTTP 403 and logged. Approved: the inputs are spent and the outputs live in the register, a velocity record of kind tree is kept at the height of approval, and the answer is {"id", "policy", "signatures": [{"digest", "signature"}]}, two per input (its checkpoint's message and the reassignment's): each a BIP340 signature by the policy key over SHA256(T ‖ T ‖ digest), T = SHA256("OpenAMP/tree-cosign"). The tag keeps these apart from every taproot signature hash the same key signs on the chain. The same request again gets the same answer. POST /v1/issuer/tree/transfer/{id}/drop gives the inputs back while no output was spent onward.

POST /v1/tree/leaves/{salt}/exited {"txid"}: closes a live leaf when that transaction, in a block, spends the leaf's own script and pays its holder's enclave the leaf's atoms at output 0. A reorganisation that takes the block away opens it again.

POST /v1/issuer/tree/clawback {"asset", "salt", "tx", "input", "reason"}: for a live leaf of an asset that elected clawback, spent at input from one of the outputs the leaf can be on the chain at, by the claw leaf, with every output of the asset paying the issuer's enclave or a permitted burn. The reason is logged before anything is signed. Answer: {"input", "sighash", "policy_sig", "leaf", "control", "txid"}, plus "issuer_sig" and the whole "witness" (<policy sig> <issuer sig> <leaf> <control>) when the server holds the issuer key (-demoissuer), else "to_sign" for the issuer. Only the leaf that holds the coin now is clawed back: a leaf already reassigned is no longer its old holder's.

Issuer surface (Authorization: Bearer <issuer token>)

Method and path Purpose
POST /v1/issuer/assets Issue a co-signed restricted asset (requires -demoissuer)
POST /v1/issuer/damp-assets Prepare a network-enforced (OpenDAMP) asset (requires -dampregistry)
POST /v1/issuer/damp-assets/{id}/complete Supply the covenant CMRs and mint it
POST /v1/issuer/damp-policy Prepare a policy update (freeze/unfreeze/admit, list or remove a contract template) for a network-enforced asset
POST /v1/issuer/damp-policy/{id}/complete Supply the recompiled CMR, the issuer signature and the respend
POST /v1/issuer/freeze Freeze or unfreeze a user
POST /v1/issuer/categories Set a user's categories
POST /v1/issuer/rules Replace an asset's policy rules
POST /v1/issuer/clawback Claw back a holder's enclave UTXOs (phase 1 when the issuer key is external)
POST /v1/issuer/clawback/{id}/complete Phase 2: submit the issuer's signatures, co-sign and broadcast
POST /v1/issuer/burn Build a redeem burn of a holder's units to an OP_RETURN output (completed through POST /v1/transfers/{id}/complete)
POST /v1/issuer/reissue Mint more of an existing asset into a target enclave, through its supply cap (idempotent by request_id)
POST /v1/issuer/supply-cap Move an issued asset's reissuance token from the server wallet into a supply cap
POST /v1/issuer/supply-cap/rekey Record a rekey of an asset's supply cap made with its cold key
POST /v1/issuer/verifier-token Move a network-enforced asset's verifier-asset token from the server wallet into its covenant
POST /v1/issuer/snapshots Publish a signed policy snapshot (network-enforced assets)
POST /v1/issuer/pledges Lock a holder's units in place as loan collateral
GET /v1/issuer/pledges List pledges
POST /v1/issuer/pledges/{id}/release Debt settled: free the collateral (lender-signed)
POST /v1/issuer/pledges/{id}/seize Default: move the collateral to the lender (lender-signed)
GET /v1/issuer/holders?asset=<id> Ownership report
POST /v1/issuer/tree Enable an asset's tree, naming its operator's key
POST /v1/issuer/tree/transfer Approve a reassignment between leaves of the tree: the rules, the register, one policy signature per message
POST /v1/issuer/tree/transfer/{id}/drop Give an approved reassignment's inputs back while none of its leaves was used
POST /v1/issuer/tree/clawback The policy half of a tree leaf's clawback, where the asset elected clawback
POST /v1/issuer/anchor Anchor the transparency-log head on-chain
POST /v1/issuer/rotate-blinding Rotate an asset's blinding-key epoch and re-import every holder's key
POST /v1/issuer/consolidate Sweep blinded change of an accepted fee asset into an explicit coin

If no issuer token is configured, every issuer request is rejected (401).

POST /v1/issuer/assets, body:

{"name": "OpenAMP Demo Bond", "ticker": "BONDX", "precision": 8,
 "atoms": 100000000000000, "holder_aid": "...", "issuer_aid": "...",
 "clawback": true, "burn_allowed": true,
 "rules": {"fee_convert_atoms": 100},
 "terms_hash": "<sha256 hex, optional>", "endpoint": "<base URL, optional>"}

Mints directly into the initial holder's enclave; clawback defaults to true and cannot be retrofitted either way. An optional "confidential": true blinds the holder's output and the fee change — it marks nothing about the asset (the contract never records it), and later transfers choose blinded or explicit independently. The reissuance token goes, explicitly, into the asset's supply cap in the same transaction: "supply_cap" is the most ever minted after issuance, in atoms (absent or 0: the supply is final at issuance), and "cap_cold_key" the x-only key that can move the covenant to a new operator key (absent: the server's -capcoldkey; refused when neither is set). Response: {"asset", "token", "entropy", "txid", "contract", "contract_hash", "supply_cap"}. Hosted issuance holds the issuer key server-side and therefore requires the -demoissuer flag (testnet demo only; a production issuer keeps that key offline).

Supply caps

Every mint goes through the asset's supply cap, built and signed by the token-covenants tool (-tokentool) with this server's operator key, which the server makes on first start in <datadir>/cap-operator.key (0600) and never prints. GET /v1/supply and GET /v1/assets/{id} show the cap:

"supply_cap": {"cap_atoms": 1000000, "minted_atoms": 400000, "room_atoms": 600000,
               "address": "<the covenant's current address>", "script_pubkey": "<hex>",
               "coin": "<txid>:<vout>", "token_atoms": 100000000,
               "issued_token_atoms": 100000000, "holds_the_whole_token": true,
               "operator_key": "<x-only>", "cold_key": "<x-only>"}

holds_the_whole_token compares the covenant's coin with the token amount the issuance created: a coin of the token anywhere else could mint outside the cap.

POST /v1/issuer/reissue, body {"asset", "target_aid", "atoms", "request_id"}, mints into the target's enclave through the covenant and answers {"reissue_txid", "status", "minted_atoms", "cap_atoms", "room_atoms"}. A mint past the cap is refused (409) before anything is built, as the covenant would refuse it on chain. "confidential": true is refused: the amount is public on the covenant's own issuance. The fee comes from an explicit wallet coin of the asset the fee rule picks.

POST /v1/issuer/supply-cap, body {"asset", "cap", "cap_cold_key"?, "token"?, "entropy"?}, moves an issued asset's token from the server wallet into a supply cap with nothing minted: every wallet coin of the token, in one transaction. An asset whose record lacks its issuance entropy and token id names them (the node wallet's listissuances shows both); the tool refuses an entropy that does not derive the asset and its token. Asking again answers with the covenant.

POST /v1/issuer/supply-cap/rekey, body {"asset", "txid", "new_operator_key", "new_cold_key"}, records a rekey the cold key's holder made with token-covenants supply-cap build (path rekey), once the tool confirms the transaction moved the recorded coin to those keys. While the covenant's operator key is not this server's, the server cannot mint.

Network-enforced issuance (OpenDAMP)

A second enforcement tier, elected per asset at issuance and committed into the asset id. Units of a network-enforced asset live in Simplicity user covenants C_U(X) and every transfer is policed on chain by a verifier covenant C_V(pi) the holder spends alongside their own coins. There is no co-signature and no enclave: transfers keep working with this policy server switched off, which is the tier's headline property. Protocol: DESIGN.md in the opendamp repository; what the shipped covenants do and do NOT enforce: STATUS.md in the opendamp repository, which holds the covenant programs and the opendamp tool — read it before describing a policy to an issuer. Every predicate in the design document — the whitelist (sender and recipient), the blacklist by outpoint, the transfer limit and the height windows — is consensus-enforced; velocity and holder caps remain registrar-side. A policy can also list contract templates the asset may enter (below), and the asset stays under every one of those predicates inside a listed contract and on the way out of it.

Configure it with -dampregistry <path> (or OPENAMPD_DAMP_REGISTRY), pointing at the CMR pinning file opendamp registry produces (vectors/addresses.json in the opendamp repository is a valid input). Unset, every network-enforcement endpoint answers 501 {"error": "network enforcement is not configured on this policy server"}. Unlike hosted issuance this needs no -demoissuer: no issuer private key is held server-side, because issuer_update_key is the issuer's own key and this server never signs a transfer of the asset.

Why it takes two calls. Both covenant programs are parameterized at compile time — the user program by (asset, verifier asset, q) and the verifier program by pi — so their CMRs come from the SimplicityHL compiler in the Rust opendamp crate, which a Go daemon cannot run. And the dependency is circular: pi commits to the asset id, the asset id commits to the contract and to the funding outpoint this server chooses, so neither the CMRs nor pi can be computed by anyone before this server has picked that outpoint. Hence:

  1. POST /v1/issuer/damp-assets issues the verifier asset V, pins the funding outpoint, and therefore fixes the asset id, the contract bytes and pi_0. It broadcasts only the V issuance and returns a derive-ready snapshot.

    {"name": "Damp Bond", "ticker": "DBND", "precision": 2, "atoms": 500000,
     "holder_pubkey": "<64-hex x-only>",
     "whitelist": ["<64-hex x-only>", "..."],
     "verifier_amount": 1,
     "issuer_update_key": "<64-hex x-only>",
     "burn_allowed": false,
     "terms_hash": "<optional>", "entity_domain": "<optional>"}

    whitelist must contain holder_pubkey: the covenant checks the recipient of every regulated output, so a holder outside the list could never be paid, not even by their own change. verifier_amount (q) defaults to 1. supply_cap and cap_cold_key configure the supply cap the asset's own reissuance token goes into at completion, as for a co-signed issuance; this server never mints a network-enforced asset, so the cap defaults to 0. network (optional) names the address encoding opendamp uses, testnet or regtest; absent, it is the node's own chain. Response: {"prepare_id", "asset", "contract", "contract_hash", "verifier_asset", "verifier_amount", "verifier_issue_txid", "pi", "whitelist_root", "tree", "snapshot", "snapshot_hash", "snapshot_sig_message", "derive_snapshot", "next"}.

    derive_snapshot is the document opendamp reads: the published snapshot (snapshot) field for field, with its pi, both list roots and their entries, plus the three things a derivation needs that the snapshot does not carry: issuer_update_key, network and the chain's genesis hash. Save it to a file as it is; nothing in it needs editing.

  2. The operator runs the Rust toolchain on the returned document, which compiles the three programs and prints their CMRs, C_V(pi_0)'s scriptPubKey and its own pi:

    opendamp derive --snapshot derive_snapshot.json
  3. POST /v1/issuer/damp-assets/{prepare_id}/complete mints the asset straight into C_U(holder) and locks q of V into C_V(pi_0) in one transaction, so the asset never exists without its verifier output and therefore never moves unpoliced. The same transaction moves the asset's reissuance token into its supply cap and V's reissuance token into its own covenant (below), so this server's wallet holds neither.

    {"user_cmr": "<u_cmr>", "verifier_cmr": "<p_cmr>", "issuer_cmr": "<g_cmr, optional>",
     "pi": "<the pi derive printed>", "verifier_spk": "<optional cross-check>",
     "snapshot_sig": "<optional BIP340 over snapshot_sig_message>"}

    pi is required and checked: this server recomputes it from the whitelist and asset id it stored and refuses (409, nothing broadcast) unless they agree. That is the authority split — this server stays authoritative about policy while the Rust toolchain stays authoritative about program compilation. A supplied verifier_spk is compared against the address derived from verifier_cmr, which catches a CMR pasted from the wrong policy before any coin moves. Response: {"asset", "verifier_asset", "txids": {"verifier_issue", "asset_issue", "verifier_lock"}, "pi", "whitelist_root", "user_covenant_address", "user_covenant_spk", "verifier_covenant_address", "verifier_covenant_spk", "contract", "contract_hash", "snapshot_seq", "snapshot_signed", "verifier_token"} — asset_issue and verifier_lock are the same transaction by construction.

The verifier asset's token. A halt burns V, and V's reissuance token is what resumes the asset; from a wallet it could mint V to any address, which would let every holder spend with no policy at all. So completion pays it, in the issuance transaction, into sequentia-contracts' verifier-token covenant (templates/verifier_token, docs/verifier-token.md) recording C_V(pi_0). Signed by the issuer update key, which this server never holds, the covenant reissues exactly q of V only into the verifier covenant it last recorded, and records only the script of a coin holding exactly q of V. A halt records the verifier it burns, so a resume restores the halted policy; opendamp's README has the halt-and-resume runbook, with token-covenants verifier-token for the covenant's side. GET /v1/assets/<id> carries the covenant under damp.verifier_token (its instance file, the verifier it records, its coin) and GET /v1/supply shows it beside the supply cap. POST /v1/issuer/verifier-token, body {"asset"}, moves V's token from this server's wallet into its covenant, recording the asset's current verifier, for an asset issued before completion did so; it derives V's issuance entropy and token from the recorded V issuance, and answers with the covenant's place when the token has moved already.

The contract's openamp block gains "enforcement": "damp", verifier_asset, verifier_amount and issuer_update_key, all committed into the asset id, and clawback is always false (there is no clawback leaf; the issuer's powers are a policy update and a halt through the covenant's issuer path). It carries no genesis_policy and no genesis_snapshot_hash: pi_0 commits to the asset id, which commits to the contract, so a contract carrying pi_0 would have to contain a hash of itself. policy_pubkey is still issued even though nothing reads it, so the asset stays registerable and the asset-id shape stays uniform across both tiers.

The genesis snapshot is published at seq 0 through the ordinary snapshot store, so GET /v1/snapshots?asset=<id> serves it like any other. Its tree is dmt-v1, the format the deployed covenants actually verify against, and a dmt-v1 snapshot's pi is the covenant's own commitment (internal-order asset bytes, the covenant rules_root) It carries BOTH list predicates, because the covenant reads both: a whitelist root over recipient keys and a blacklist root over frozen outpoints. The genesis blacklist is empty but not absent — the empty interval tree still has a root (its guard interval), so "freeze nothing" is a commitment like any other and a missing root is a malformed policy rather than a permissive one. Blacklist entries are SHA256(txid || BE32(vout)) with the txid in internal (consensus) byte order, not the reversed display form. Later policy versions go through POST /v1/issuer/snapshots and require the issuer signature as always.

Transaction shape limits are part of the covenant. The verifier program is compiled once per transaction shape and every shape is a leaf of the same taptree: the canonical leaf is p3x5 (3 inputs, 5 outputs, one regulated input) and the menu runs through p3x4 and p4x6 to p5x7 (5 inputs, 7 outputs, three regulated inputs), so a single transfer may spend at most three UTXOs of the asset. That is a real operational constraint on a holder with fragmented coins, and it is committed into the program identity, so it cannot be widened for an asset already issued against it. The shapes and their bounds are pinned in opendamp's vectors/addresses.json under programs.verifier_shapes.

Policy updates: the freeze path for a network-enforced asset

A co-signed asset is frozen by withholding a signature. A network-enforced one has no signature to withhold, so a freeze is a publication plus a spend: publish snapshot seq n+1 whose whitelist drops the holder (the covenant checks the owner key of every regulated input) and/or whose blacklist lists an outpoint (that UTXO alone stops moving), then respend the verifier output through the issuer path G(I) so the on-chain C_V commits to the new pi. Until that respend confirms, holders transfer under the OLD policy, because the old C_V is what their transfers spend. Both directions are reversible by a further update.

This hits the same seam issuance does and resolves it the same way, so a policy update is also two phases.

  1. POST /v1/issuer/damp-policy takes a DELTA against the currently published policy, never a replacement set:

    {"asset": "<id>",
     "add_whitelist": ["<key>", {"key": "<key>", "send_after": 95200, "recv_after": 0}],
     "remove_whitelist": ["<key>"],
     "set_windows": [{"key": "<key>", "send_after": 95200, "recv_after": 0}],
     "add_blacklist": [{"txid": "<display txid>", "vout": 3}],
     "remove_blacklist": [{"txid": "<display txid>", "vout": 3}],
     "add_contracts": ["seqdex/guarded-order"],
     "remove_contracts": ["<template name or hash>"],
     "reason": "court order 2026-1188"}

    reason is required and is written to the public transparency log BEFORE anything is signed, exactly as a clawback's is. The endpoint recomputes both roots and pi_{n+1} from the snapshot chain, builds and validates snapshot n+1, records a pending update, and returns {"policy_id", "seq", "prev_pi", "pi_next", "whitelist", "blacklist", "contracts", "change", "snapshot", "to_sign", "verifier_outpoint", "verifier_cmr_current", "derive_snapshot", "derive_current", "next"}. derive_snapshot is snapshot n+1 and derive_current the published snapshot n, each in the form opendamp reads (as at issuance). Nothing is signed, published or broadcast. Refusals are the requests that would publish a policy the issuer did not mean: an empty holder list, a duplicate addition, a removal of something not listed, a contract template the network's verifier cannot recognise, a change that changes nothing.

  2. The issuer's toolchain runs opendamp derive --snapshot <derive_snapshot> for the new p_cmr and C_V(pi_{n+1}) address, and opendamp issuer-update --snapshot <derive_current> --next-snapshot <derive_snapshot> --request <fee coin> --issuer-privkey <key> for the finished, signed respend. The issuer brings its own fee coin: this is the issuer's transaction, not the operator's.

  3. POST /v1/issuer/damp-policy/{id}/complete with {"sig", "verifier_cmr", "verifier_spk": "<optional>", "signed_tx"}. sig is the issuer update key's BIP340 signature over to_sign (the tagged snapshot hash), which is the same signature every published seq after 0 has always required, and it is checked FIRST. verifier_cmr must differ from the current one — an unchanged CMR means the derivation ran against the old policy — and a supplied verifier_spk must equal the address derived from it. signed_tx is checked structurally: it must consume the recorded verifier outpoint and recreate C_V at the new address carrying exactly q of the verifier asset. Only then does it run the mempool gate, broadcast, and publish seq n+1. Replaying a completed update returns the same txid with "idempotent": true and never broadcasts or publishes twice.

    The authority split is issuance's, unchanged: this server cannot compile Simplicity and cannot produce a G(I) witness, so it is authoritative about POLICY (both roots, pi, the sequence, the reason, the record) and accepts a program identity it cannot compile only alongside the checks it can make.

Contract templates. add_contracts lists a template the asset may enter, by its name or its template hash; remove_contracts takes it off. The template a policy can list is SeqDEX's guarded order, seqdex/guarded-order (opendamp's DESIGN.md section 2.4, and docs/seqdex-guarded-order.md in sequentia-contracts). It is the only one because a contract can hold the asset only if every spend of it needs the verifier at input 0. Under a listing policy:

  • a whitelisted holder moves the asset into an order they own;
  • anyone fills it, for a whitelisted recipient;
  • the owner takes it back after expiry.

The owner's whitelist entry, lockup and limit and the outpoint blacklist bind the order's coin as they bind a user covenant. Taking the template off the list freezes the asset already in orders until it is listed again. The snapshot carries the list as predicates.contracts, template hashes in ascending order, and pi commits to it. A policy that lists nothing omits the key, so it has the bytes and the pi it always had. A genesis policy lists nothing, and the issuer lists a template with an update. opendamp order-address and opendamp contract-verifier build an order and its spends from the published document. e2e/damp_contracts.py runs the whole path.

Height bounds live in the whitelist leaf. A whitelist entry is SHA256(0x00 || key || BE32(send_after) || BE32(recv_after)): send_after is the lockup binding the OWNER of a regulated input, recv_after the receive window binding the RECIPIENT of a regulated output, and zero means unbounded. In the snapshot document an unbounded entry is a bare hex string and a bounded one an object, so every document written before bounds existed still hashes and verifies exactly as it did. Computing a whitelist root from keys alone would publish a commitment that omits every bound, which is a pi no deployed program answers to.

On a network-enforced asset, every co-signing path — POST /v1/transfers, POST /v1/cosign, /v1/issuer/burn, /v1/issuer/reissue, /v1/issuer/clawback — answers 409 {"error": "this asset is network-enforced: transfers are built by the holder from the published snapshot, not co-signed here"}. The read paths stay useful instead of reporting an empty enclave: GET /v1/users/{aid}/address returns the holder's covenant address (no leaves, and ?confidential=1 is refused because the covenant must read explicit asset ids), and balance, holders and supply are scanned at the covenant scripts.

Rules object (co-signed tier only; used at issuance and by POST /v1/issuer/rules with body {"asset": "<id>", "rules": {...}}):

{"allowed_categories": ["accredited"],
 "velocity_window_blocks": 1000, "velocity_max_atoms": 500000,
 "holder_cap": 50,
 "lockin_until_height": 12000, "convert_during_lockin": true,
 "vesting": [{"aid": "...", "atoms": 1000000, "until_height": 20000}],
 "fee_convert_atoms": 100}

All fields optional; zero/empty means no restriction.

POST /v1/issuer/freeze: {"aid": "...", "frozen": true}. POST /v1/issuer/categories: {"aid": "...", "categories": ["accredited"]}.

POST /v1/issuer/clawback: {"asset": "<id>", "holder_aid": "...", "reason": "<required>"}. The reason is written to the public transparency log before the transaction is signed; the seized funds move to the issuer's enclave through the disclosed clawback leaf. Fails for assets issued without a clawback leaf. Response: {"txid", "atoms"} when the server holds the issuer key (-demoissuer). For an asset whose issuer key is external (every asset issued through SeqPal) the call is phase 1 and returns {"id", "tx", "to_sign": [{"input", "sighash", "pubkey"}], "atoms"}; the issuer signs each sighash with its entity key and posts {"sigs": {"<input index>": "<sig hex>"}} to POST /v1/issuer/clawback/{id}/complete, which adds the policy signature, broadcasts and returns {"txid"} (a replay returns the same txid with "idempotent": true).

Pledges (collateral). A restricted asset cannot be moved into a lending covenant without leaving its enclave, so collateral for a loan is locked in place: POST /v1/issuer/pledges {"asset", "holder_aid", "lender_aid", "atoms", "debt_asset", "debt_atoms", "maturity_height", "note"} records the pledge and the policy engine keeps those atoms unspendable by the holder. The issuer token alone cannot move value: /release {"repaid_txid", "lender_sig"} needs the lender's signature (or "force": true with a logged reason, which only ever returns the collateral to its owner), and /seize {"reason", "lender_sig", "holder_sig"} needs the lender's signature plus either a matured loan or the holder's countersignature. Signatures are BIP340 over sha256("openamp-pledge|<action>|<id>|<extra>"), never a taproot sighash. A seizure is a clawback-leaf spend paying the pledged atoms to the lender's enclave and the rest back to the holder; with an external issuer key it is two-phase like a clawback and completes through POST /v1/issuer/clawback/{id}/complete.

GET /v1/issuer/holders?asset=<id> returns {"asset", "height", "holders": {"<aid>": atoms}, "total_atoms"} from a confirmed UTXO-set scan.

POST /v1/issuer/anchor (empty body) commits OPENAMP:<seq>:<log head hash> in an OP_RETURN and returns {"txid", "seq", "head"}.

Transparency log

GET /v1/log serves the raw log: one JSON object per line, {"seq", "prev", "time", "action", "data", "hash"}, where hash = sha256("<seq>|<prev>|<time>|<action>|<data-json>") and prev is the previous entry's hash. Any client can re-verify the chain and compare its head against the latest on-chain anchor.

Note on contract history: BONDX was issued before the contract-v1 freeze, so its on-chain contract JSON carries a legacy "tier": "A" field. Assets issued while confidentiality was per-asset carry a "confidential" key in their openamp block; new issuances never write one (confidentiality is per transfer). Verify every contract as-is over its exact committed bytes; new issuances follow spec/contract-v1.md.

End-to-end walkthrough

Against your own openampd (see "Build, run, test" and deploy/DEPLOY.md); the live instance works the same for the unauthenticated endpoints. keygen and signer are demo helpers from this repository.

# 0. demo keys (name, private key, x-only pubkey) for issuer, alice, bob
go run ./openampd/cmd/keygen
# issuer 7f3a... 8fcd...
# alice  92b1... 55aa...
# bob    0cc4... 77ee...

# 1. register the three accounts (returns AIDs)
curl -s localhost:8722/v1/users -d '{"pubkeys":["<issuer-xonly>"]}'
curl -s localhost:8722/v1/users -d '{"pubkeys":["<alice-xonly>"]}'
curl -s localhost:8722/v1/users -d '{"pubkeys":["<bob-xonly>"]}'

# 2. issuer mints 1,000,000.00000000 BONDX into alice's enclave
curl -s localhost:8722/v1/issuer/assets \
  -H "Authorization: Bearer $ISSUER_TOKEN" \
  -d '{"name":"OpenAMP Demo Bond","ticker":"BONDX","precision":8,
       "atoms":100000000000000,"holder_aid":"<alice-aid>",
       "issuer_aid":"<issuer-aid>","burn_allowed":true,
       "rules":{"fee_convert_atoms":100},"supply_cap":50000000000000}'
# -> {"asset":"<asset-id>", "txid":..., "contract":..., "contract_hash":...,
#     "supply_cap":{"cap_atoms":50000000000000, "minted_atoms":0, ...}}

# 3. alice's enclave address and balance for the new asset
curl -s "localhost:8722/v1/users/<alice-aid>/address?asset=<asset-id>"
curl -s "localhost:8722/v1/users/<alice-aid>/balance?asset=<asset-id>"

# 4. alice sends 5000 atoms to bob, fee converted from the asset
curl -s localhost:8722/v1/transfers \
  -d '{"asset":"<asset-id>","sender_aid":"<alice-aid>",
       "recipient_aid":"<bob-aid>","atoms":5000,"fee_mode":"convert"}'
# -> {"id":"<tid>", "to_sign":[{"input":0,"sighash":"<h>", ...}], ...}

# 5. alice signs each sighash with her enclave key and completes
go run ./openampd/cmd/signer <alice-priv> <h>       # -> <64-byte sig hex>
curl -s localhost:8722/v1/transfers/<tid>/complete \
  -d '{"sigs":{"0":"<sig-hex>"}}'
# -> {"txid":"..."}   (or 403 with the policy refusal reason)

# 6. the decision trail
curl -s localhost:8722/v1/log

The same flow, driven programmatically against a regtest node, is the committed integration proof test/functional/feature_openamp_daemon.py in the node repository.

How openampd uses the Sequentia node

openampd needs one Sequentia node (sequentiad) with a funded wallet, and the token-covenants tool for the supply caps; an asset's tree also needs the arca-enclave tool. There are no consensus changes and no patched node. An explorer (electrs) instance is optional: it is only the prevout fallback for POST /v1/cosign when the node runs without -txindex. RPC usage:

  • chain queries: getblockcount, getblockhash, getblock (follower), gettxout (prevout resolution and the supply cap's coin), getrawtransaction (the coins handed to the supply-cap tool), scantxoutset (enclave balances and holder reports), decodescript (address rendering), getfeeexchangerates and dumpassetlabels (the assets the node accepts for fees, and its rate for each);
  • wallet operations: listunspent, getnewaddress, getaddressinfo, signrawtransactionwithwallet, sendrawtransaction, and createrawtransaction + fundrawtransaction for log anchors.

The node runs one scantxoutset at a time, so openampd queues its own scans (a wallet that reads every restricted balance at once gets each answer in turn) and waits up to 30 seconds for a scan that another program started on the same node, answering 502 only if that scan is still running.

Enclave transactions are built by openampd's own minimal Elements codec (openampd/internal/elements): explicit assets and values end to end, Elements taproot with the /elements tagged hashes and leaf version 0xc4, and the Elements taproot sighash. Byte-exactness is enforced by golden vectors generated from the node repository's functional-test framework. Issuance entropy and asset/token IDs are derived in openampd/internal/fastmerkle (the Elements two-leaf fast merkle root).

Fees follow Sequentia's open fee market: a transaction pays its fee in exactly one asset, which each node values at its own rate, and no asset is the default. openampd chooses the fee asset of every transaction it funds by the rule every Sequentia wallet uses: among the assets the node accepts for fees (getfeeexchangerates), the one whose largest explicit coin in openampd's wallet covers the most fees, valued at the node's rate (a tie goes to the lower asset id). The policy asset is one candidate among the others. The asset a transaction moves would come first if the node accepted it, but every asset openampd moves is a restricted asset, its reissuance token or a verifier asset, and none of them ever pays a fee (Rule 1), even on a node that rates one. The fee is -feesats, a value in the node's reference unit, paid as ceil(feesats × 1e8 / rate) atoms of the chosen asset, so it is worth the same whichever asset pays. When no coin of an accepted asset covers it, the request is refused (503) with a line naming the assets the node accepts, and nothing is built; each choice is logged (fee: <atoms> atoms of <asset>, from <coin>). -feeasset pins one asset instead: openampd refuses to start when the node does not accept it, and refuses a transaction when the node has stopped accepting it since.

Build, run, test

Requires Go 1.26+ (dependencies are vendored, so builds work offline).

git clone https://github.com/ConcatenaLabs/openamp.git
cd openamp
go build ./...
go test ./...
go build -o openampd/openampd ./openampd/cmd/openampd

Run against a node:

./openampd/openampd \
  -rpc http://127.0.0.1:18776 \
  -rpcauth user:pass \            # or cookie:/path/to/.cookie
  -rpcwallet mywallet \
  -issuertoken <long-random-token> \
  -demoissuer                     # testnet demos only: issuer keys server-side
  -signer frost                   # 2-of-3 threshold policy key (default: local)
Flag Default Meaning
-listen 127.0.0.1:8722 HTTP listen address
-datadir ~/.openampd state directory (state.json, keys.json 0600, transparency.log)
-rpc http://127.0.0.1:18776 sequentiad RPC URL (the node's RPC port on chain test)
-rpcauth $OPENAMPD_RPCAUTH (required) user:pass or cookie:<path>; set it in the environment, since a command line is readable by every user on the machine
-rpcwallet (none) wallet name, appended as /wallet/<name>
-issuertoken $OPENAMPD_ISSUER_TOKEN, else none bearer token gating /v1/issuer/*; empty locks the issuer API; set it in the environment, as for -rpcauth
-feeasset $OPENAMPD_FEE_ASSET, else none display hex of the one asset openampd pays fees in, refused at start unless the node accepts it for fees; unset, each transaction's fee asset follows the wallet rule (see "How openampd uses the Sequentia node")
-feesats 1000 the fee of each server-funded transaction, valued in the node's reference unit (atoms at a rate of 1e8) and paid in its fee asset at the node's rate
-tokentool $OPENAMPD_TOKEN_TOOL path to token-covenants (sequentia-contracts' tools/token-covenants); unset, issuance and minting are refused
-capcoldkey $OPENAMPD_CAP_COLD_KEY default cold key (x-only hex) of a new supply cap, held off this server
-capoperatorkey <datadir>/cap-operator.key this server's supply-cap operator key file, made on first start
-demoissuer off hold issuer keys server-side (testnet demo only)
-signer local (OPENAMPD_SIGNER) policy-key backend: local (one key per asset) or frost (2-of-3 threshold quorum, DKG-generated keys; OPENAMPD_FROST_KEYGEN=dealer reverts to the trusted-dealer split)
-dampregistry (none) path to the OpenDAMP CMR pinning file (OPENAMPD_DAMP_REGISTRY); unset disables network enforcement
-electrs http://127.0.0.1:3003 (OPENAMPD_ELECTRS_URL) explorer (electrs) base URL; prevout fallback for /v1/cosign when the node lacks -txindex
-arcaenclave $OPENAMPD_ARCA_ENCLAVE path to arca-enclave, built from the arca repository, which reads the boards and transfers of an asset's tree; unset, no asset has a tree here and every tree call answers 501
-follow 2s chain follower poll interval

Production deployment (systemd unit, Caddy reverse proxy, secrets handling): deploy/DEPLOY.md.

Repository layout

openampd/cmd/openampd/      the daemon (flags, wiring, chain follower start)
openampd/cmd/keygen/        demo BIP340 keypair generator
openampd/cmd/signer/        demo client-side sighash signer
openampd/internal/server/   HTTP API, policy engine, issuance, transfers,
                            clawback, pledges, snapshots, PolicySigner seam,
                            chain follower
openampd/internal/server/frostsigner/  the FROST 2-of-3 policy-key backend
                            (DKG, signing, Member/Transport seam)
openampd/internal/server/tree.go  an asset's tree: admission, the register,
                            reassignments, exits, clawback
openampd/internal/server/treefake/  a stand-in for arca-enclave in tests
openampd/internal/server/treerate/  the measured rate of tree approvals
                            (OPENAMPD_TREE_RATE=1)
openampd/internal/damp/     OpenDAMP policy commitment (pi), dmt-v1 tree,
                            snapshot document format
openampd/internal/elements/ minimal Elements tx codec, taproot, sighash
                            (golden-vectored; see tools/gen_vectors.py)
openampd/internal/fastmerkle/  issuance entropy and asset/token id derivation
openampd/internal/rpc/      minimal JSON-RPC client for sequentiad
openampd/internal/store/    JSON state store, 0600 key file, transparency log
openampd/docs/              design notes (blinding-key rotation, the M2
                            snapshot service)
e2e/supply_cap.py           the supply cap through a real openampd and node
e2e/verifier_token.py       a network-enforced asset's verifier-asset token
                            through a real openampd and node
openampd/testdata/          the opendamp repository's CMR pinning file, pinned
                            for the tests that check the Go mirrors against it
spec/                       frozen formats (contract v1) and the venue/wallet
                            integration specification
deploy/                     systemd units + deploy runbook
tools/gen_vectors.py        golden-vector generator (runs against the node
                            repo's functional-test framework)
vendor/                     vendored Go dependencies (offline builds)

The SeqPal gateway, seqpald, lives in the SeqPal repository.

Regenerating the golden vectors (needs a checkout of the node repository):

PYTHONPATH=$SEQ_REPO/test/functional python3 tools/gen_vectors.py \
  > openampd/internal/elements/testdata/vectors.json
go test ./openampd/internal/elements

Integration proofs

  • test/functional/feature_openamp_m0.py (node repo): the M0 proof; demonstrates on regtest that enclave issuance, policy-co-signed transfer, clawback, and the contract-to-asset-ID binding all work against unmodified consensus.
  • test/functional/feature_openamp_daemon.py (node repo): end-to-end integration of a real openampd process with a regtest node, covering the hosted-transfer, rules, freeze, and clawback flows.
  • e2e/supply_cap.py: a real openampd and a regtest node with Simplicity active from genesis: an asset issued with a supply cap and its token in the covenant from the issuance on, mints through the reissue endpoint up to the cap exactly, a mint one atom over refused in a block forced with generateblock beside a control the node accepts, and a mint the server wallet tries alone refused. It needs a node checkout, the token-covenants tool, and seqc with a sequentia-contracts checkout (its docstring has the command).
  • e2e/verifier_token.py: a real openampd, a regtest node with Simplicity active from genesis, and the opendamp and token-covenants tools: a network-enforced asset issued in its two phases with V's token in its covenant recording C_V(pi_0) and the asset's token in its supply cap from the issuance on, the wallet holding neither; a halt that burns V and records the verifier, a resume through the covenant, and a transfer that settles again. It needs a node checkout, both tools and opendamp's CMR pinning file (its docstring has the command).
  • openampd/internal/elements/testdata/vectors.json: golden vectors proving the Go Elements primitives byte-exact against the node's test framework.

Ecosystem

Repo One-liner
Sequentia The Sequentia node (Sequentia Core, sequentiad; a fork of Elements 23.3.3): consensus, anchoring, proof of stake, open fee market, plus the canonical protocol documentation in doc/sequentia/.
sequentia-registry Sequentia Asset Registry service (asset metadata).
SWK Sequentia Wallet Kit: a fork of Blockstream LWK, providing a Rust wallet library, CLI, and WASM bindings for building Sequentia (and Bitcoin testnet4) wallets.
ambra Ambra: non-custodial dual-chain (Bitcoin testnet4 + Sequentia) mobile wallet, a Flutter UI over a Rust core built on SWK.

Contributing

Pull requests against main. Run go build ./... && go test ./... before submitting; if you touch openampd/internal/elements, regenerate or extend the golden vectors rather than weakening the tests. Secrets (RPC credentials, issuer tokens, keys) never belong in this repository.

About

OpenAMP: open-source restricted-asset issuance and transfer-approval service for Sequentia (an AMP2 equivalent) with opt-in confidentiality. Live API at sequentiatestnet.com/openamp/v1/

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages