Atto Node is the network-facing service for the Atto cryptocurrency. It exposes a public HTTP API for submitting/querying blocks (transactions), maintains account state in a MySQL database, connects to other nodes over WebSockets, and participates in consensus by producing and relaying votes.
This repository is a Kotlin + Spring Boot (WebFlux) application with a small embedded Ktor WebSocket/HTTP server for node-to-node communication.
- What this node does
- Quick start (local dev)
- Configuration
- Ports
- API
- How consensus works (high level)
- Bootstrapping / catching up
- Code map
- Docker images
- Native image metadata
- Troubleshooting
At a high level, this service:
- Accepts transactions via REST (
POST /transactions) and via the peer network (WebSocket messages). - Validates incoming transactions against account state.
- If valid and confirmed by the network, it applies them to the account ledger state stored in MySQL.
- Participates in voting/consensus (if configured as a voter) by producing signed votes and gossiping them.
- Bootstraps / discovers missing data by requesting and replaying missing transactions from peers.
The node can behave as:
- Voter: signs and broadcasts votes (requires sufficient voting weight and a configured signer key).
- Historical: stores final votes / serves as a data source for bootstrap flows.
- API node: exposes the REST API (this project does by default).
(Some roles can be forced via config; see application-default.yaml and application.yaml.)
- Java 25 (the Gradle toolchain is pinned to 25 in
build.gradle.kts) - Docker available locally (required by Testcontainers)
The quickest way to run the node locally is TestApplication (src/test/kotlin/cash/atto/node/TestApplication.kt). Run
that class directly from your IDE; it starts the node and spins up MySQL automatically through Testcontainers.
On startup, the node will:
- run Flyway migrations against the configured MySQL
- start the REST API
- start the node-to-node WebSocket server
Configuration is split across application.yaml and profile overrides such as:
application-default.yaml(opinionated defaults for local)application-local.yaml(local developer settings)application-dev.yaml,application-beta.yaml,application-live.yaml
The main config namespace is atto.*.
The service uses R2DBC for runtime access and JDBC for Flyway:
ATTO_DB_HOST(defaultlocalhostin local/default profiles)ATTO_DB_PORT(default3306)ATTO_DB_NAME(defaultatto)ATTO_DB_USER(defaultroot)ATTO_DB_PASSWORD(default empty)
The node announces its public URI to peers:
ATTO_PUBLIC_URI(required byapplication.yaml)
In application-default.yaml the default is:
atto.node.public-uri = ws://localhost:${websocket.port}
Voting requires a signer backend:
ATTO_SIGNER_BACKEND(defaultlocal)ATTO_PRIVATE_KEY(used by local signer backend)
There is also support for a remote signer:
ATTO_SIGNER_REMOTE_URLATTO_SIGNER_REMOTE_TOKEN
To peer with other nodes, configure seed URIs:
ATTO_DEFAULT_NODES
In application-dev.yaml there are example wss://... nodes.
From application.yaml:
- 8080: public REST API (Spring WebFlux)
- 8081: management/metrics (Actuator + Prometheus)
- 8082: node-to-node WebSocket server (Ktor)
Swagger UI is configured at:
:8080/(seespringdoc.swagger-ui.path: /)
This project exposes endpoints for:
- Transactions: submit, query, and stream
POST /transactionsPOST /transactions/stream(publish + stream confirmation)GET /transactions/{hash}GET /transactions/stream(NDJSON live stream)
- Accounts: query and stream account snapshots
POST /accountsGET /accounts/{publicKey}POST /accounts/stream(NDJSON)
- Receivables, Account Entries, Vote weight, and time utilities
All streaming endpoints use NDJSON (application/x-ndjson) and combine:
- database results (historical)
- in-memory flows of live updates (reactive push)
Atto’s consensus in this codebase is vote-driven:
- Nodes observe competing transactions for the same
(account publicKey, height). - A node that is configured as a voter and has enough voting weight produces signed votes.
- Votes are gossiped across the network; once cumulative weight passes a configurable threshold, the network considers consensus reached.
Key parts in this repo:
cash.atto.node.election.*- ElectionVoter: decides when/what to vote for, signs votes, broadcasts them.
- Includes a stability delay to avoid chasing rapidly changing provisional consensus.
- ElectionProcessor: when consensus is reached, it commits the winning transactions into account state (and stores final votes if this node is historical).
- ElectionVoter: decides when/what to vote for, signs votes, broadcasts them.
cash.atto.node.vote.weight.VoteWeighter- Maintains the weight map and computes thresholds like minimal confirmation weight.
Important thresholds (see application.yaml under atto.vote.weight.*):
minimal-rebroadcast-weightminimal-confirmation-weightconfirmation-threshold(percentage of online weight)
A node must be able to recover when:
- it missed messages
- it receives transactions without dependencies
- it is behind the head of an account chain
This repo implements bootstrap as a set of discoverers and an unchecked transaction pipeline.
When the node learns about a transaction it cannot immediately apply (missing dependencies, out-of-order, etc.), it stores it as an unchecked transaction:
cash.atto.node.bootstrap.unchecked.*UncheckedTransactionProcessorStarterruns every second, pulls oldest unchecked txs, validates them against current account state, and applies what it can.
-
Dependency discovery (
DependencyDiscoverer)- When a transaction is rejected for a recoverable reason, it is tracked.
- If enough final votes accumulate for that transaction, it is re-introduced as “discovered” even though it was initially rejected.
-
Gap discovery (
GapDiscoverer)- For accounts with gaps, the node requests a range of missing transactions from historical peers using
AttoTransactionStreamRequest/Response. - Handles out-of-order deliveries with an in-memory buffer.
- For accounts with gaps, the node requests a range of missing transactions from historical peers using
-
Head/last discovery (
LastDiscoverer)- Historical nodes periodically broadcast a sample of recent “head” transactions (
AttoBootstrapTransactionPush). - If a receiving node is behind, it starts a lightweight vote stream for the head to confirm it’s real, then turns it
into a
TransactionDiscoveredevent.
- Historical nodes periodically broadcast a sample of recent “head” transactions (
-
Bounded persistence queue (
DiscoveryQueue,DiscoveryPersistenceWorker)DiscoveryQueuededuplicates and buffers discovered transactions.- New gap, head, and send discovery pauses at the configured target capacity. Replies already in flight can use the configured headroom and suspend without being discarded when the bounded queue is full.
DiscoveryPersistenceWorkerpersists batches of up to 1,000 transactions and retries the same batch after a database failure.
The overall effect: nodes can reconnect, observe the network’s latest heads, request missing ranges, and replay into their ledger state.
Common entry points:
src/main/kotlin/cash/atto/node/Application.kt— Spring Boot entrysrc/main/kotlin/cash/atto/node/ApplicationConfiguration.kt— scheduling, OpenAPI, runtime hints
Major modules (packages):
network/— peer connections, handshake, WebSocket server/client, keep-alivestransaction/— transaction ingest, validation, prioritization, REST controlleraccount/— account snapshot storage and streamingelection/— elections, voting, and applying consensus resultsvote/— vote persistence and weight calculationsbootstrap/— gap/dependency/head discovery and unchecked transaction recovery
This repo includes multiple Dockerfiles:
Dockerfile/Dockerfile.*variants
Example: Dockerfile.dev builds on ghcr.io/attocash/node and sets:
SPRING_PROFILES_ACTIVE=dev,json
If you want a “container-first” run, pick the profile and pass env vars for DB + ATTO_PUBLIC_URI.
CI uses committed GraalVM reachability metadata from
src/main/resources/META-INF/native-image/cash.atto/node/reachability-metadata.json. It does not run the native-image
agent in the release pipeline because the GraalVM 25 agent is too slow for the full integration suite.
Regenerate the metadata locally only from a successful full agent run:
rm -rf build/native/agent-output/test
JAVA_HOME=/var/home/felipe/.jdks/graalvm-jdk-25 \
GRADLE_USER_HOME=/tmp/gradle-home \
./gradlew -Pagent test --no-daemon --info --fail-fast
mkdir -p src/main/resources/META-INF/native-image/cash.atto/node
cp build/native/agent-output/test/reachability-metadata.json \
src/main/resources/META-INF/native-image/cash.atto/node/reachability-metadata.jsonDo not commit metadata from a failed test run. After copying the file, verify it with:
grep -n 'CounterChallengeResponse\$\$serializer' \
src/main/resources/META-INF/native-image/cash.atto/node/reachability-metadata.json
./gradlew test --no-daemon --fail-fast
NATIVE_IMAGE_OPTIONS="--static --libc=musl --gc=G1 -R:MaxRAMPercentage=90" \
./gradlew nativeCompile --no-daemonThe Actuator health indicator marks the node down if it has been disconnected for too long:
cash.atto.node.network.NetworkHealthIndicator
Fixes:
- set
ATTO_DEFAULT_NODESto validws://orwss://seed URIs - ensure
ATTO_PUBLIC_URIis correct and reachable - check that port 8082 is reachable from other peers
Common causes:
- You’re on
LOCALnetwork without a private key set (not a VOTER node)
See LICENSE.