Skip to content

Add core protocol packages, blockchain adapters, the p2p transport layer, and CI#1

Merged
philanton merged 15 commits into
masterfrom
feat/core
Jun 17, 2026
Merged

Add core protocol packages, blockchain adapters, the p2p transport layer, and CI#1
philanton merged 15 commits into
masterfrom
feat/core

Conversation

@philanton

@philanton philanton commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

The protocol primitives plus the consumer-facing blockchain layer (deposit +
withdrawal across EVM, BTC, XRPL, and Solana), the p2p transport layer, a
real-node integration harness, and CI.

Protocol primitives (pkg/)

  • core — wire types (blocks, ops, receipts, URIs, Slot) with generated
    tuple-mode CBOR codecs; chain-agnostic adapter interfaces
  • decimal, cborx — fixed-point decimal + canonical envelope/frame codec
  • abiutil, eip712 — ABI type singletons + EIP-712 digest/recovery
  • bls — BN254 keygen/sign/aggregate/verify, cluster-signature verification,
    vault withdrawal-ID derivation
  • receipt — burn/mint receipt digests and verifier
  • log — structured, context-aware Logger interface (zap / noop / span
    implementations); every package logs through it, defaulting to no-op so the
    library is silent until a consumer injects a logger
  • signing-preimage freeze: byte-exact golden tests enforced by
    scripts/ci/check-preimage-goldens.sh

Signer seam (pkg/sign)

Pluggable, algorithm-aware Signer (secp256k1/ed25519) with an in-memory
KeySigner for clients/CLI/tests and EVM digest helpers; KMS backends satisfy
the same interface.

Blockchain adapters (pkg/blockchain)

Per-concern adapters over caller-supplied chain clients + a sign.Signer. The
deposit path is core.VaultDepositor (SubmitDeposit to broadcast, plus a
VerifyDeposit tri-state read — absent / pending / confirmed); withdrawal is
core.VaultWithdrawalFinalizer (Pack/Validate/Sign/Submit/
VerifyExecution) — a custody node runs it over a caller-orchestrated quorum,
with only signature collection left to the caller's mesh. Submit merges the
collected signatures and broadcasts in one step. Signer rotation mirrors that
shape as core.SignerRotationFinalizer (Pack/Validate/Sign/Submit/
VerifyRotation), implemented for all four chains. Pack/Validate take an
opaque opID — in-place chains bind replay on-chain and ignore it; BTC embeds
it in the sweep (below).

  • EVM — generated bindings (regenerated from vendored ABI/bytecode via
    go generate; see pkg/blockchain/evm/artifacts/README.md), on-chain BLS
    pubkey cache, Depositor/WithdrawalFinalizer/RotationFinalizer/Registry/Token/Fraud/Faucet
  • BTC — m-of-n P2WSH multisig vault: depositor + finalizer + rotation (a
    UTXO sweep into the newly-derived vault, behind a VaultStore seam, marked
    with an OP_RETURN(opID) so a watcher can attribute the landed sweep), plus a
    concrete bitcoind JSON-RPC Client. Canonical validators assert the fixed
    SIGHASH fields (version / locktime / final sequences).
  • XRPL — multi-sign vault: depositor + finalizer + rotation (SignerListSet)
    (+ TicketProvider seam, with a ledger-backed LedgerTicketProvider). Wire
    helpers (BuildAmount, CanonicalJSON, DeriveIdentity, ValidateCanonical,
    Identity) exported for non-finalizer callers. Validation rejects a non-zero
    Flags (tfPartialPayment underdelivery guard), and submit filters the
    collected blobs against the vault's live SignerList + quorum.
  • Solana — custody Anchor program: depositor (native SOL + SPL) + finalizer
    (native SOL and SPL) + rotation; ed25519 quorum verified on-chain via the
    Ed25519 precompile. SPL withdrawals add the recipient-ATA creation + token
    remaining-accounts and can ride a v0 transaction over an Address Lookup Table
    (VaultLookupAddresses returns the table's invariant account set) so large
    quorums fit the packet limit. Bindings generated from the vendored Anchor IDL
    via go generate (anchor-go); on-chain commitment configurable.

P2P transport layer (pkg/p2p)

The canonical libp2p wire contract, pinned to one protocol version, as a
library over a caller-supplied host.Host — the SDK registers handlers and
speaks the wire; it never constructs the Host or owns connectivity.

  • protocol — stream IDs + GossipSub topic names (pinned), the
    AuthChallenge/AuthResponse/ReceiptAck wire structs with handwritten CBOR
    codecs (golden-frozen), and the Registrar interface every server implements
  • auth — the /ynp/auth handshake with two roles: operator (secp256k1
    signature recovered against an allow-list) and passive (libp2p identity key).
    Server (HandleAuth + Register) and Client (Authenticate)
  • receipt — burn/mint receipt submission over a ReceiptHandler seam.
    Server (HandleBurnReceipt/HandleMintReceipt + Register) and Client
    (SendBurnReceipt/SendMintReceipt)
  • pubsub — generic publish/subscribe over any cborx-envelope payload, type
    parameterized via constraint inference (Publisher[T]/Follower[T], e.g.
    *core.FinalizedWithdrawal)

Devnet & integration tests (devnet/)

make devnet brings up anvil + bitcoind + rippled + solana-test-validator and
blocks until ready; make integration runs self-provisioning deposit +
withdrawal flows per chain (fresh keys/accounts/contract each run — re-runs are
clean), including an SPL-token withdrawal over a v0/ALT transaction. Devnet
images are pinned by digest. See devnet/README.md.

CI (.github/workflows)

Reusable test-go (unit: go test -race ./...) and test-integration
(make devnetmake integrationmake devnet-down) workflows, invoked by
per-event callers on pull requests and pushes to master. Test-only.

Make targets

build / lint / test / generate / devnet / devnet-down / integration

🤖 Generated with Claude Code

philanton and others added 2 commits June 12, 2026 18:18
Initial Go module with the protocol primitives:

- pkg/core: wire types (blocks, ops, receipts, URIs) with generated
  tuple-mode CBOR codecs
- pkg/decimal: arbitrary-precision decimal with tag-2 CBOR encoding
- pkg/cborx: canonical envelope/frame/version codec
- pkg/abiutil: shared ABI type singletons
- pkg/eip712: EIP-712 digest and signer recovery
- pkg/bls: BN254 keygen/sign/aggregate/verify, cluster signature
  verification, vault withdrawal-ID derivation
- pkg/receipt: burn/mint receipt digests and verifier
- preimage freeze: byte-exact golden tests for signing preimages,
  enforced by scripts/ci/check-preimage-goldens.sh
Builds the consumer-facing blockchain layer on top of the protocol
primitives, plus a real-node integration harness.

- pkg/sign: pluggable, algorithm-aware Signer (secp256k1/ed25519) with an
  in-memory KeySigner for clients/CLI/tests and EVM digest helpers; KMS
  backends satisfy the same interface
- pkg/core: chain-agnostic adapter interfaces (VaultDepositor,
  VaultWithdrawalFinalizer with Pack/Validate/Sign/Merge/Submit/
  VerifyExecution, Registry/Token/Fraud/Faucet readers+writers) and Slot types
- pkg/blockchain/evm: generated contract bindings (regenerated from vendored
  ABI/bytecode via `go generate`), on-chain BLS pubkey cache, and per-concern
  adapters — Depositor, WithdrawalFinalizer, Registry, Token, Fraud, Faucet
- pkg/blockchain/btc: m-of-n P2WSH multisig vault — depositor + finalizer
- pkg/blockchain/xrpl: multi-sign vault — depositor + finalizer
- devnet/: docker-compose (anvil + bitcoind + rippled) with a readiness gate;
  self-provisioning deposit + withdrawal integration tests per chain
- Makefile: build / lint / test / generate / devnet / integration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@philanton philanton changed the title Add core protocol packages Add core protocol packages and the blockchain adapter layer Jun 16, 2026
philanton and others added 3 commits June 16, 2026 13:52
- pkg/blockchain/sol: VaultDepositor (native SOL + SPL) and
  VaultWithdrawalFinalizer (native SOL) over the custody Anchor program — an
  ed25519 quorum signs a digest, verified on-chain via the Ed25519 precompile;
  fee payer separate from the quorum signers
- generated program bindings (pkg/blockchain/sol/custody) emitted from the
  vendored Anchor IDL by `go generate` via an anchor-go-backed idl_refresher
  (the Solana parallel of the EVM abi_refresher); IDL + program binary vendored
  under sol/artifacts
- on-chain read/submit commitment is configurable (default Finalized;
  devnet/tests use Confirmed to avoid waiting for finality)
- devnet: solana-test-validator service with the program preloaded upgradeable
  at its fixed id + a readiness probe; self-provisioning deposit + withdrawal
  integration test (toolchain-free at test time)
- evm: WithdrawalFinalizer.Submit now waits for the execute tx to mine

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the chain-agnostic adapter surface:

- core.DepositStatus tri-state + VaultDepositor.VerifyDeposit per chain
  (EVM receipt depth, BTC confirmations, XRPL validated flag, Solana
  commitment ladder).
- Concrete btc.Client (bitcoind JSON-RPC) with typed RPCError; the
  withdrawal idempotency check now branches on the error code.
- Export the XRPL wire helpers (BuildAmount, CanonicalJSON,
  DeriveIdentity, ValidateCanonical) and the Identity type for
  non-finalizer callers.
- Add xrpl.LedgerTicketProvider, a client-backed TicketProvider.

Fold VaultWithdrawalFinalizer.Merge into Submit(packed, signatures),
dropping the unused merged-bytes intermediate, and rename
VaultDepositor.Deposit to SubmitDeposit to pair with VerifyDeposit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add core.SignerRotationFinalizer (Pack/Validate/Sign/Submit/VerifyRotation)
and a per-chain implementation:

- EVM: updateSigners over the live quorum; rotation digest commits to
  chainId, vault, "updateSigners", keccak(newSigners,newThreshold) and the
  on-chain signerNonce, golden-tested against the contract.
- Solana: update_signers with the ed25519 quorum verified via the Ed25519
  precompile; digest binds the signers commitment + program nonce.
- XRPL: multi-signed SignerListSet; replay defense is the account sequence.
- BTC: no in-place form, so rotation is a sweep of every vault UTXO into the
  newly-derived vault, behind the same interface via a VaultStore seam that
  pivots on confirmation.

Share the EVM quorum-signature merge (mergeQuorumSigs/fetchLiveQuorum) and
the XRPL multisign combine between the withdrawal and rotation paths, and
add abiutil.AddressArr for the rotation digest.

Per-chain rotation integration tests run on the devnet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dimast-x

Copy link
Copy Markdown
Collaborator

Blockchain-adapter adoption: chain-by-chain, NOT uniform ⚠️

This is where the real findings are. Artifact-level (digest/preimage/signature/address) the SDK matches custody almost everywhere — but adoption has blockers:

│ XRPL │ ⚠️ Artifact-clean, │ SDK Validate drops the Flags==0 check → reopens ISS-002 (tfPartialPayment │
│ │ hardening gaps │ underdelivery); combine ignores live SignerList │

│ SOL │ ⚠️ Artifact-clean, feature │ Same Anchor program & digests, but SDK rejects SPL withdrawals and has no │
│ │ regression │ ALT/v0 tx (large quorums exceed the 1232-byte cap) │
│ BTC │ ❌ Rotation is a hard │ Deposits/withdrawals byte-identical (incl. the OP_DROP per-user deposit │
│ │ blocker │ scheme ✅), but rotation sweep diverges — see below │

The BTC rotation blocker (verified): SDK builds a 1-output sweep with no marker; custody builds [newVault(total-fee), OP_RETURN(requestID)] and its withdrawal watcher recognizes the landed sweep by that OP_RETURN requestID to pivot the vault. An SDK-built sweep would have a different txid, strip the marker, and never be recognized → vault never pivots. Either the SDK sweep must emit the OP_RETURN, or custody keeps its own BTC rotation finalizer.

@dimast-x

Copy link
Copy Markdown
Collaborator

Ask ai to review today's commits from the custody repo (what they affect) and apply this diff towards SDK too

Adds the pkg/p2p protocol layer over a caller-supplied host.Host:

- protocol: stream IDs + topic names pinned to one version, the
  AuthChallenge/AuthResponse/ReceiptAck wire structs with handwritten
  CBOR codecs (golden-frozen), and the Registrar interface.
- auth: /ynp/auth handshake with operator (secp256k1 + allow-list) and
  passive (libp2p identity) roles; Server + Client.
- receipt: burn/mint submission over a ReceiptHandler seam; Server with
  per-stream HandleBurnReceipt/HandleMintReceipt + Client.
- pubsub: concrete FinalizedWithdrawal publish/subscribe.
- gossip: generic publish/subscribe over any cborx payload — the
  type-parameterized alternative to pubsub.

Servers take a host and register handlers; they never construct the
Host. pubsub and gossip overlap by design — one is dropped before merge.
@philanton philanton changed the title Add core protocol packages and the blockchain adapter layer Add core protocol packages, blockchain adapters, and the p2p transport layer Jun 17, 2026
Adds pkg/log — a structured, context-aware Logger interface with zap,
noop, and span (OpenTelemetry) implementations — and routes every
logging call through it.

- pkg/p2p (auth/receipt/pubsub/gossip): loggers take log.Logger; nil
  defaults to a no-op so the library is silent unless a logger is
  injected.
- evm BLSPubkeyCache: replace global slog calls with an injected
  logger field, defaulting to no-op, settable via SetLogger; the
  constructor signature is unchanged.
The BTC rotation sweep now emits the watcher-recognizable wire: output 0
pays the new vault, the final output is a zero-value OP_RETURN carrying
the rotation's operation id. A watcher can attribute the landed sweep to
a specific rotation by that marker and pivot the vault — a single
unmarked output could not be distinguished from any other vault spend.

SignerRotationFinalizer.Pack/Validate take an opID [32]byte. BTC embeds
it in the sweep; EVM, XRPL, and Solana bind rotation replay on-chain
(signerNonce / account sequence / program nonce) and accept opID only to
keep one uniform signature. VerifyRotation stays keyed on the new signer
set, so it remains a direct state read on every chain.
XRPL: reject a present-but-non-zero (or non-numeric) Flags in both the
withdrawal and rotation canonical validators, closing the
tfPartialPayment underdelivery path on issued-currency withdrawals. The
multi-sign combine now reads the vault's live SignerList and trims to the
live SignerQuorum, dropping blobs from signers that have rotated off (or
not yet on) and sizing the fee to the current quorum.

Solana: support SPL-token withdrawals — an idempotent recipient-ATA
creation ahead of the Ed25519 companion and the token remaining-accounts
on execute (token program, vault ATA, recipient ATA), reusing the
generated instruction's data encoding. Submit can emit a v0 transaction
over a configured Address Lookup Table so large quorums stay within the
1232-byte packet limit.

BTC: the withdrawal validator asserts the fixed fields the SIGHASH
commits to (tx version, zero locktime, final input sequences), rejecting
a non-final or RBF-signalling canonical tx before signing.

Docs: the Custody (EVM) and custody-program (Solana) artifacts are now
sourced from the custody repo, not clearnet.
Tests for the previously-uncovered logic plus the matching BTC fix:

- BTC: factor the SIGHASH fixed-field checks into validateFixedTxFields
  and apply them in the rotation validator too (they were only on the
  withdrawal path); unit test for the version/locktime/sequence guard.
- XRPL: unit tests for the Flags==0 guard on both the withdrawal and
  rotation validators (tfPartialPayment rejected), and for the
  live-SignerList blob filter using real multi-sign blobs.
- Solana: unit test pinning the execute account shape (native vs the SPL
  remaining-accounts), and an SPL-withdrawal case in the integration test
  that mints to the vault ATA and asserts the recipient ATA is credited.
…an ALT

VaultLookupAddresses returns the invariant accounts of the execute
instruction — the lookup-table-eligible set that lets large quorums fit a
v0 transaction. Centralizing it in the SDK keeps the table in lockstep
with the instruction's account layout, instead of every consumer
hand-copying the list.

The SPL-withdrawal integration case now builds a real Address Lookup
Table from VaultLookupAddresses and withdraws over it, exercising the
v0/ALT submit path end to end. The table's recent_slot is taken at
finalized commitment, the only slot guaranteed to already be in the
SlotHashes sysvar (the current slot can equal the execution slot and is
rejected as not recent).
Reusable test-go (go test -race ./...) and test-integration (make devnet
→ make integration → make devnet-down) workflows, invoked by per-event
callers on pull requests and pushes to master. Test-only — no lint,
build, or publish steps.

Pin every devnet image to its manifest-list digest so the integration
job runs the exact versions the suite was validated against and a moving
upstream tag cannot break CI out from under unrelated changes.
@philanton philanton changed the title Add core protocol packages, blockchain adapters, and the p2p transport layer Add core protocol packages, blockchain adapters, the p2p transport layer, and CI Jun 17, 2026
- protocol: ReceiptAck decoder accepts >=2 array elements and skips the
  trailing fields instead of requiring exactly 2. A real clearnode emits a
  wider (6-element) ack, which the strict reader rejected — this blocked
  the receipt client from talking to production. The encoder still writes
  the 2-element form, so the golden vector is unchanged.
- auth: the server bounds the whole handshake with a deadline (challenge
  write + response read), matching the receipt server, so a stalled peer
  cannot pin a handler goroutine.
- log: ZapLogger.WithKV clones the parent's key-value slice before
  appending; the bare append reused the parent's backing array, racing and
  corrupting sibling loggers' KV under fan-out.
Resolve the side-by-side pubsub/gossip choice in favour of the generic,
type-parameterized implementation: remove the concrete
*core.FinalizedWithdrawal pubsub and rename gossip → pubsub. The package
now serves any cborx-envelope payload (FinalizedWithdrawal included) via
Publisher[T]/Follower[T].
- bls: DeserializeG1/G2 are acceptance-path decoders for untrusted input
  but set coordinates without checks. Reject non-canonical coordinates
  (>= field prime), off-curve points, and points outside the prime-order
  subgroup — the membership the on-chain precompile enforces. Without the
  subgroup check a crafted point could pass off-chain acceptance.
- evm: packedFromOp rejects a recipient or asset that is not a well-formed
  hex address. common.HexToAddress silently zero-fills a malformed input,
  which would otherwise sign a withdrawal to the wrong (often zero)
  destination.
@philanton philanton merged commit cf90fc4 into master Jun 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants