
Immutable, append-only, cryptographically linked chain for tamper-evident local data sync with content deduplication, HTTP/TCP sync modules, reactive updates, pluggable persistence and conflict merging.
A Kotlin Multiplatform library for building append-only, cryptographically linked data structures with built-in synchronization capabilities.
Upchain is an immutable, append-only chain of updates where each link is cryptographically hashed to the previous one. This creates a tamper-evident data structure similar to a blockchain but designed for local data synchronization between devices.
An Upchain is an immutable chain of Update items. Each item contains:
update: The actual data (a string value)hash: A cryptographic hash linking this item to the previous oneval upchain = Upchain.empty
.plus(Update("First update"))
.plus(Update("Second update"))Each update's hash is computed as:
hash[n] = SHA256(hash[n-1] + update[n])
This creates a chain where any modification to historical data would break all subsequent hashes, making tampering detectable.
Updates with identical content are automatically deduplicated using content hashing. Adding the same update twice will not create a duplicate entry.
Upchain uses a repository pattern with a mediator for persistence:
┌─────────────────┐
│ UpchainRepository │
│ (in-memory) │
└────────┬────────┘
│ StateFlow<Upchain>
│
▼
┌─────────────────┐
│ UpchainMediator │
│ (persistence) │
└─────────────────┘
The UpchainRepository provides:
upchain: StateFlow<Upchain> - Reactive access to the current chain stateeditWithResult() - Thread-safe modifications with mutex lockingThe mediator handles persistence operations:
append(updates) - Append new updates to storage (efficient for logs)replace(updates) - Replace entire chain (used on divergence)When the chain diverges (e.g., a conflicting update is applied), the mediator uses replace() to rewrite the entire storage. Otherwise, it uses append() for efficiency.
Upchain detects divergence by comparing hashes:
// Divergence example
val current = Upchain.empty + Update("A") + Update("B")
val diverged = Upchain.empty + Update("A") + Update("C") // Different branchManages multiple upchains by ID:
createUpchain(id) - Create a new upchainupchains: StateFlow<List<Item>> - Reactive list of all upchainsid, repository, and remove() functionimport org.hnau.upchain.core.*
import org.hnau.upchain.core.repository.upchain.*
// Create an empty upchain
var upchain = Upchain.empty
// Add updates
upchain = upchain + Update("Hello")
upchain = upchain + Update("World")
// Access items
println(upchain.items.size) // 2
println(upchain.peekHash) // Latest hashimport org.hnau.upchain.core.repository.upchain.*
import kotlinx.coroutines.flow.collectLatest
// Create a repository with in-memory mediator
val repository = UpchainRepository.create(
updates = emptyList(),
mediator = object : UpchainMediator {
override suspend fun append(updates: NonEmptyList<Update>) {
// Persist to your storage
}
override suspend fun replace(updates: List<Update>) {
// Replace entire chain
}
}
)
// Observe changes
repository.upchain.collectLatest { upchain ->
println("Chain has ${upchain.items.size} items")
}
// Add updates
repository.addUpdate(Update("New data"))
// Or add multiple
repository.addUpdates(listOf(
Update("Update 1"),
Update("Update 2")
))import org.hnau.upchain.core.repository.file.upchain.fileBased
// Create a file-backed repository
val repository = UpchainRepository.fileBased(
filename = "/path/to/chain.txt"
)
// All updates are automatically persisted to the file
repository.addUpdate(Update("Persisted update"))import org.hnau.upchain.core.repository.file.upchains.fileBased
import org.hnau.upchain.core.UpchainId
import kotlin.uuid.Uuid
// Create a repository managing multiple upchains in a directory
val upchainsRepo = UpchainsRepository.fileBased(
dir = "/path/to/chains/"
)
// Create a new upchain
val newId = UpchainId.createRandom()
upchainsRepo.createUpchain(newId)
// Access all upchains
upchainsRepo.upchains.collectLatest { items ->
items.forEach { item ->
println("Upchain ${item.id}: ${item.repository.upchain.value.items.size} items")
}
}
// Remove an upchain
val item = upchainsRepo.upchains.value.first()
item.remove()// Network synchronization mediator
class NetworkMediator(
private val client: HttpClient,
private val endpoint: String
) : UpchainMediator {
override suspend fun append(updates: NonEmptyList<Update>) {
client.post("$endpoint/append") {
setBody(updates.map { it.value })
}
}
override suspend fun replace(updates: List<Update>) {
client.post("$endpoint/replace") {
setBody(updates.map { it.value })
}
}
}The :sync modules provide synchronization between upchain instances over the network with support for multiple transport protocols.
:sync:core - Shared sync protocol and API
:sync:tcp - TCP transport utilities (CBOR serialization, channel extensions)
:sync:http - HTTP transport utilities (JSON serialization, response mappers)
:sync:server:core - Server-side synchronization logic
:sync:server:tcp - TCP server implementation
:sync:server:http - HTTP server implementation
:sync:client:core - Client-side synchronization logic
:sync:client:tcp - TCP client implementation
:sync:client:http - HTTP client implementation
The synchronization protocol uses a three-way sync algorithm:
┌─────────────┐ ┌─────────────┐
│ Client │ ◄────────────────► │ Server │
│ (pull) │ GetUpchains │ │
│ │ GetUpdates │ │
│ │ AppendUpdates │ │
└─────────────┘ └─────────────┘
Pull Phase: Client downloads updates from server starting from common hash
Push Phase: Client uploads local updates to server
When client and server have diverged (different updates after common base):
Client: A → B → C (local)
↓
Server: A → B → D (remote)
After sync: A → B → D → C (merged)
Both client and server end up with the merged chain containing all updates.
HTTP transport uses JSON over HTTP POST requests. Suitable for:
Default port: 80 (can use any port)
Server:
import org.hnau.upchain.sync.server.core.*
import org.hnau.upchain.sync.server.core.repository.*
import org.hnau.upchain.sync.server.http.httpSyncServer
import org.hnau.upchain.sync.core.ServerPort
// Start HTTP sync server
val result = httpSyncServer(
api = ServerSyncApi(scope, repository.toCreateOnly(scope)),
port = ServerPort(80), // or ServerPort.defaultHttp
)Client:
import org.hnau.upchain.sync.client.core.sync
import org.hnau.upchain.sync.client.http.HttpSyncClient
import org.hnau.upchain.sync.core.ServerAddress
import org.hnau.upchain.sync.core.ServerPort
// Create HTTP client
val api = HttpSyncClient(
scope = this,
address = ServerAddress("upchain.example.com"),
port = ServerPort(80),
)
// Sync a repository
val result = repository.sync(
id = upchainId,
api = api,
)TCP transport uses CBOR serialization over raw TCP sockets. Suitable for:
Default port: 26385
Server:
import org.hnau.upchain.sync.server.core.*
import org.hnau.upchain.sync.server.core.repository.*
import org.hnau.upchain.sync.server.tcp.tcpSyncServer
import org.hnau.upchain.sync.core.ServerPort
// Start TCP sync server
val result = tcpSyncServer(
api = ServerSyncApi(scope, repository.toCreateOnly(scope)),
port = ServerPort.default, // port 26385
)Client:
import org.hnau.upchain.sync.client.core.sync
import org.hnau.upchain.sync.client.tcp.TcpSyncClient
import org.hnau.upchain.sync.core.ServerAddress
import org.hnau.upchain.sync.core.ServerPort
// Create TCP client
val api = TcpSyncClient(
scope = this,
address = ServerAddress("192.168.1.100"),
port = ServerPort.default, // port 26385
)
// Sync a repository
val result = repository.sync(
id = upchainId,
api = api,
)The sync protocol uses sealed class messages:
GetUpchains - Request list of available upchains from serverGetMaxToMinUpdates - Request updates in reverse order with paginationAppendUpdates - Push updates to server (with hash validation)All messages are serialized using JSON (HTTP) or CBOR (TCP).
org.hnau.upchain.core
├── Upchain # Immutable chain data structure
├── Update # Value class for update content
├── UpchainHash # Cryptographic hash of chain position
├── UpchainId # UUID-based identifier for upchains
└── utils
├── ContentHash # Content-based deduplication hash
└── SHA256 # Platform-specific hashing
org.hnau.upchain.core.repository
├── upchain
│ ├── UpchainRepository # Repository interface
│ ├── UpchainMediator # Persistence mediator
│ └── FileBasedUpchainRepository # File implementation
└── upchains
├── UpchainsRepository # Multi-chain repository
└── FileBasedUpchainsRepository # File implementation
org.hnau.upchain.sync
├── core
│ ├── SyncApi # Sync API interface
│ ├── SyncHandle # Sealed protocol messages
│ └── ServerPort # Port configuration
├── tcp # TCP transport utilities
│ ├── SyncConstantsTcp # CBOR serialization
│ ├── ChannelExt # Byte channel helpers
│ └── ApiResponseExt # Response serialization
├── http # HTTP transport utilities
│ ├── SyncConstantsHttp # JSON serialization
│ ├── ApiResponseExt # Response mappers
│ └── HttpScheme # HTTP scheme enum
├── client
│ ├── core
│ │ ├── sync() # Extension for syncing repository
│ │ ├── ServerUpdatesProvider
│ │ └── RemoteUpdatesSink
│ ├── tcp
│ │ └── TcpSyncClient # TCP client implementation
│ └── http
│ └── HttpSyncClient # HTTP client implementation
└── server
├── core
│ ├── ServerSyncApi # Request dispatcher
│ ├── UpchainSyncServer # Single upchain handler
│ └── UpchainsSyncServer # Multi-upchain handler
├── tcp
│ └── tcpSyncServer() # TCP server entry point
└── http
└── httpSyncServer() # HTTP server entry point
MIT License
A Kotlin Multiplatform library for building append-only, cryptographically linked data structures with built-in synchronization capabilities.
Upchain is an immutable, append-only chain of updates where each link is cryptographically hashed to the previous one. This creates a tamper-evident data structure similar to a blockchain but designed for local data synchronization between devices.
An Upchain is an immutable chain of Update items. Each item contains:
update: The actual data (a string value)hash: A cryptographic hash linking this item to the previous oneval upchain = Upchain.empty
.plus(Update("First update"))
.plus(Update("Second update"))Each update's hash is computed as:
hash[n] = SHA256(hash[n-1] + update[n])
This creates a chain where any modification to historical data would break all subsequent hashes, making tampering detectable.
Updates with identical content are automatically deduplicated using content hashing. Adding the same update twice will not create a duplicate entry.
Upchain uses a repository pattern with a mediator for persistence:
┌─────────────────┐
│ UpchainRepository │
│ (in-memory) │
└────────┬────────┘
│ StateFlow<Upchain>
│
▼
┌─────────────────┐
│ UpchainMediator │
│ (persistence) │
└─────────────────┘
The UpchainRepository provides:
upchain: StateFlow<Upchain> - Reactive access to the current chain stateeditWithResult() - Thread-safe modifications with mutex lockingThe mediator handles persistence operations:
append(updates) - Append new updates to storage (efficient for logs)replace(updates) - Replace entire chain (used on divergence)When the chain diverges (e.g., a conflicting update is applied), the mediator uses replace() to rewrite the entire storage. Otherwise, it uses append() for efficiency.
Upchain detects divergence by comparing hashes:
// Divergence example
val current = Upchain.empty + Update("A") + Update("B")
val diverged = Upchain.empty + Update("A") + Update("C") // Different branchManages multiple upchains by ID:
createUpchain(id) - Create a new upchainupchains: StateFlow<List<Item>> - Reactive list of all upchainsid, repository, and remove() functionimport org.hnau.upchain.core.*
import org.hnau.upchain.core.repository.upchain.*
// Create an empty upchain
var upchain = Upchain.empty
// Add updates
upchain = upchain + Update("Hello")
upchain = upchain + Update("World")
// Access items
println(upchain.items.size) // 2
println(upchain.peekHash) // Latest hashimport org.hnau.upchain.core.repository.upchain.*
import kotlinx.coroutines.flow.collectLatest
// Create a repository with in-memory mediator
val repository = UpchainRepository.create(
updates = emptyList(),
mediator = object : UpchainMediator {
override suspend fun append(updates: NonEmptyList<Update>) {
// Persist to your storage
}
override suspend fun replace(updates: List<Update>) {
// Replace entire chain
}
}
)
// Observe changes
repository.upchain.collectLatest { upchain ->
println("Chain has ${upchain.items.size} items")
}
// Add updates
repository.addUpdate(Update("New data"))
// Or add multiple
repository.addUpdates(listOf(
Update("Update 1"),
Update("Update 2")
))import org.hnau.upchain.core.repository.file.upchain.fileBased
// Create a file-backed repository
val repository = UpchainRepository.fileBased(
filename = "/path/to/chain.txt"
)
// All updates are automatically persisted to the file
repository.addUpdate(Update("Persisted update"))import org.hnau.upchain.core.repository.file.upchains.fileBased
import org.hnau.upchain.core.UpchainId
import kotlin.uuid.Uuid
// Create a repository managing multiple upchains in a directory
val upchainsRepo = UpchainsRepository.fileBased(
dir = "/path/to/chains/"
)
// Create a new upchain
val newId = UpchainId.createRandom()
upchainsRepo.createUpchain(newId)
// Access all upchains
upchainsRepo.upchains.collectLatest { items ->
items.forEach { item ->
println("Upchain ${item.id}: ${item.repository.upchain.value.items.size} items")
}
}
// Remove an upchain
val item = upchainsRepo.upchains.value.first()
item.remove()// Network synchronization mediator
class NetworkMediator(
private val client: HttpClient,
private val endpoint: String
) : UpchainMediator {
override suspend fun append(updates: NonEmptyList<Update>) {
client.post("$endpoint/append") {
setBody(updates.map { it.value })
}
}
override suspend fun replace(updates: List<Update>) {
client.post("$endpoint/replace") {
setBody(updates.map { it.value })
}
}
}The :sync modules provide synchronization between upchain instances over the network with support for multiple transport protocols.
:sync:core - Shared sync protocol and API
:sync:tcp - TCP transport utilities (CBOR serialization, channel extensions)
:sync:http - HTTP transport utilities (JSON serialization, response mappers)
:sync:server:core - Server-side synchronization logic
:sync:server:tcp - TCP server implementation
:sync:server:http - HTTP server implementation
:sync:client:core - Client-side synchronization logic
:sync:client:tcp - TCP client implementation
:sync:client:http - HTTP client implementation
The synchronization protocol uses a three-way sync algorithm:
┌─────────────┐ ┌─────────────┐
│ Client │ ◄────────────────► │ Server │
│ (pull) │ GetUpchains │ │
│ │ GetUpdates │ │
│ │ AppendUpdates │ │
└─────────────┘ └─────────────┘
Pull Phase: Client downloads updates from server starting from common hash
Push Phase: Client uploads local updates to server
When client and server have diverged (different updates after common base):
Client: A → B → C (local)
↓
Server: A → B → D (remote)
After sync: A → B → D → C (merged)
Both client and server end up with the merged chain containing all updates.
HTTP transport uses JSON over HTTP POST requests. Suitable for:
Default port: 80 (can use any port)
Server:
import org.hnau.upchain.sync.server.core.*
import org.hnau.upchain.sync.server.core.repository.*
import org.hnau.upchain.sync.server.http.httpSyncServer
import org.hnau.upchain.sync.core.ServerPort
// Start HTTP sync server
val result = httpSyncServer(
api = ServerSyncApi(scope, repository.toCreateOnly(scope)),
port = ServerPort(80), // or ServerPort.defaultHttp
)Client:
import org.hnau.upchain.sync.client.core.sync
import org.hnau.upchain.sync.client.http.HttpSyncClient
import org.hnau.upchain.sync.core.ServerAddress
import org.hnau.upchain.sync.core.ServerPort
// Create HTTP client
val api = HttpSyncClient(
scope = this,
address = ServerAddress("upchain.example.com"),
port = ServerPort(80),
)
// Sync a repository
val result = repository.sync(
id = upchainId,
api = api,
)TCP transport uses CBOR serialization over raw TCP sockets. Suitable for:
Default port: 26385
Server:
import org.hnau.upchain.sync.server.core.*
import org.hnau.upchain.sync.server.core.repository.*
import org.hnau.upchain.sync.server.tcp.tcpSyncServer
import org.hnau.upchain.sync.core.ServerPort
// Start TCP sync server
val result = tcpSyncServer(
api = ServerSyncApi(scope, repository.toCreateOnly(scope)),
port = ServerPort.default, // port 26385
)Client:
import org.hnau.upchain.sync.client.core.sync
import org.hnau.upchain.sync.client.tcp.TcpSyncClient
import org.hnau.upchain.sync.core.ServerAddress
import org.hnau.upchain.sync.core.ServerPort
// Create TCP client
val api = TcpSyncClient(
scope = this,
address = ServerAddress("192.168.1.100"),
port = ServerPort.default, // port 26385
)
// Sync a repository
val result = repository.sync(
id = upchainId,
api = api,
)The sync protocol uses sealed class messages:
GetUpchains - Request list of available upchains from serverGetMaxToMinUpdates - Request updates in reverse order with paginationAppendUpdates - Push updates to server (with hash validation)All messages are serialized using JSON (HTTP) or CBOR (TCP).
org.hnau.upchain.core
├── Upchain # Immutable chain data structure
├── Update # Value class for update content
├── UpchainHash # Cryptographic hash of chain position
├── UpchainId # UUID-based identifier for upchains
└── utils
├── ContentHash # Content-based deduplication hash
└── SHA256 # Platform-specific hashing
org.hnau.upchain.core.repository
├── upchain
│ ├── UpchainRepository # Repository interface
│ ├── UpchainMediator # Persistence mediator
│ └── FileBasedUpchainRepository # File implementation
└── upchains
├── UpchainsRepository # Multi-chain repository
└── FileBasedUpchainsRepository # File implementation
org.hnau.upchain.sync
├── core
│ ├── SyncApi # Sync API interface
│ ├── SyncHandle # Sealed protocol messages
│ └── ServerPort # Port configuration
├── tcp # TCP transport utilities
│ ├── SyncConstantsTcp # CBOR serialization
│ ├── ChannelExt # Byte channel helpers
│ └── ApiResponseExt # Response serialization
├── http # HTTP transport utilities
│ ├── SyncConstantsHttp # JSON serialization
│ ├── ApiResponseExt # Response mappers
│ └── HttpScheme # HTTP scheme enum
├── client
│ ├── core
│ │ ├── sync() # Extension for syncing repository
│ │ ├── ServerUpdatesProvider
│ │ └── RemoteUpdatesSink
│ ├── tcp
│ │ └── TcpSyncClient # TCP client implementation
│ └── http
│ └── HttpSyncClient # HTTP client implementation
└── server
├── core
│ ├── ServerSyncApi # Request dispatcher
│ ├── UpchainSyncServer # Single upchain handler
│ └── UpchainsSyncServer # Multi-upchain handler
├── tcp
│ └── tcpSyncServer() # TCP server entry point
└── http
└── httpSyncServer() # HTTP server entry point
MIT License