@leofcoin/peernet
v3.1.1
Published
Peer-to-peer networking for Leofcoin and distributed applications.
Readme
peernet
Peer-to-peer networking for Leofcoin and distributed applications.
Features
- Decentralized peer discovery and messaging
- Pluggable request handlers
- File and data sharing
- Works in Node.js, browser, and Electron
Installation
Breaking content-address format in 2.0
Peernet 2.x uses @leofcoin/codec-format-interface 2.x. All generated content
addresses use canonical codec → digest byte length → digest headers. Length-first
hashes are not supported by the hash parser. Stored 1.x content-address indexes
and linked manifests are not automatically migrated or rewritten.
Upgrade all message producers and consumers together. For a chain deployment, coordinate the messages, workers, browser node and genesis migration before starting against new state. Back up existing stores and owner identities; this package does not delete or reset them. Do not mix old content-addressed chainstate with the new format or treat an installation update as a completed network reset.
npm install @leofcoin/peernetUsage
import Peernet from '@leofcoin/peernet'
const config = {
network: 'leofcoin:peach',
networkVersion: 'peach',
// Optional per-installation transport identity; defaults to the wallet identity.
routingId: 'lfc-peer-a1b2c3d4',
stars: ['wss://peach.leofcoin.org']
}
// If your identity is password-protected, prompt for password:
import passwordPrompt from '@leofcoin/peernet/src/prompts/password/node.js';
const password = await passwordPrompt();
const node = await new Peernet(config, password)
await node.start()
console.log(globalThis.peernet)node.id is the routing identity used for peer discovery and swarm connections.
When routingId is omitted it remains the wallet identity for backward
compatibility. node.peerId always identifies the loaded wallet and should be
used by applications that need the wallet identity, such as chain transaction
signing. Applications with multiple installations of the same wallet can
provide a distinct, persisted routingId for each installation.
Encrypted chat archive mode
Applications that already create and verify encrypted chat envelopes can opt in
to Peernet's bounded ciphertext archive. Archive settings and records are keyed
by the complete Peernet network string (including its version, such as
leofcoin:peach) in the message store. Archive mode is disabled by default.
Records expire after 30 days and are limited to 500 messages per network.
Plaintext message fields are rejected; the application supplies verification
functions for the signed envelope, inbox request and delivery receipt.
const archive = node.createArchiveMode({
verifyEnvelope,
verifyInboxRequest,
verifyReceipt
})
await archive.setEnabled(true) // Explicit opt-in for this node and network.
archive.onMessage(({ envelope }) => {
// Decrypt the envelope in the application, then acknowledge with a signed receipt.
})
await archive.start({ account, deviceId })
await archive.requestInbox(signedInboxRequest)
// The envelope must contain ciphertext, recipient account/device, timestamp,
// signature, and either `network` or a network-bound device certificate.
await archive.send(encryptedEnvelope)
await archive.acknowledge(signedDeliveryReceipt)Inbox requests and receipts must be signed and verified by the application. Archives forward only ciphertext and never decrypt messages. Disabling archive mode deletes this network's locally archived copies; it does not revoke or erase copies on other nodes. Delivery and best-effort deletion depend on reachable peers.
API
addRequestHandler
Register a handler for custom requests:
peernet.addRequestHandler('lastBlock', async () => {
const height = await chainStore.get('localIndex')
const hash = await chainStore.get('localBlock')
const response = JSON.stringify({ height: height.toString(), hash: hash.toString() })
return new ResponseMessage({ response })
// or
// return new peernet.protos['peernet-response']({ response })
})
peernet.addRequestHandler('hello', () => {
return new ResponseMessage({ response: 'hi' })
})
peernet.addRequestHandler('version', () => {
return new ResponseMessage({ response: { version: 1 } })
})Password Prompt
For password-protected identities, use the provided prompt:
- Node.js:
import passwordPrompt from '@leofcoin/peernet/src/prompts/password/node.js' - Browser:
import passwordPrompt from '@leofcoin/peernet/src/prompts/password/browser.js'
const password = await passwordPrompt();
const node = await new Peernet(config, password);In-Memory Broadcasts: API and Usage
What is it?
The broadcast method allows you to share files or folders with peers, storing them in-memory (not persisted to disk). Data is available for direct retrieval by hash until the process restarts.
API:
// For files:
peernet.broadcast(path: string, { content: Uint8Array }): Promise<string>
// For folders:
peernet.broadcast(path: string, { links: Array<{ path: string, hash: string }> }): Promise<string>Parameters:
path(string): The virtual path or identifier for the file or folder.content(Uint8Array): The file data (for files).links(Array): An array of{ path, hash }objects (for folders).
Returns:
A Promise that resolves to a hash string, which can be used to retrieve the data.
Examples
Broadcast a file:
const hash = await peernet.broadcast('/hello.txt', {
content: new TextEncoder().encode('Hello world!')
});Files larger than 256 KiB are automatically split into independently hashed pieces. The returned hash identifies a manifest, so receivers can verify every piece and request only pieces they still need. Override the piece size with chunkSize:
const hash = await peernet.broadcast('/video.mp4', {
content: videoBytes,
chunkSize: 1024 * 1024
})For a pausable/resumable download, use the transfer controller:
const transfer = peernet.download(hash, { pin: true })
const unsubscribe = transfer.onProgress(({ transferredBytes, totalBytes, state }) => {
console.log(state, transferredBytes, totalBytes)
})
transfer.pause()
transfer.resume() // already completed pieces are not fetched again
const bytes = await transfer.result
unsubscribe()peernet.cat(hash) also reassembles chunked files transparently. pin: true caches verified pieces in the local data store, allowing a later transfer instance to reuse them.
Downloads pipeline four chunks by default. Known manifest providers are reused for all chunk hashes and chunk requests rotate across those peers, allowing different pieces to arrive concurrently from different providers. Configure the queue with transferConcurrency. Chunk size adapts to payload size (256 KiB normally, 1 MiB from 32 MiB, and 4 MiB from 256 MiB); an explicit chunkSize always overrides it.
Large blocks
Blocks larger than 1 MiB are also chunked internally, without changing the public block API or the canonical block hash supplied by the chain:
await peernet.block.put(blockHash, encodedBlock)
const encodedBlockAgain = await peernet.block.get(blockHash)
const transfer = peernet.block.download(blockHash)
transfer.pause()
transfer.resume()
const bytes = await transfer.resultThe block manifest remains stored under the canonical block hash; individual chunks have their own transport hashes. Peernet verifies every chunk before reconstruction. The chain layer must still verify the reconstructed canonical block hash and perform normal consensus validation before loading it. Defaults can be configured with blockChunkThreshold and blockChunkSize in the Peernet constructor.
Broadcast a folder:
const folderHash = await peernet.broadcast('/my-folder', {
links: [
{ path: '/hello.txt', hash: hash }
]
});Retrieve data by hash (peer side):
const proto = { decoded: { hash } };
await peernet.handleData(peer, 'some-id', proto);
// The peer will receive either .data (for files) or .links (for folders)Notes
- If you broadcast a folder, only the
linksproperty is required;contentshould be omitted. - If you broadcast a file, only the
contentproperty is required;linksshould be omitted. - The hash returned is unique to the content or folder structure.
- Data is lost when the process restarts.
In-Memory Broadcasts
Peernet supports in-memory data sharing for fast, temporary distribution of files or folders between peers. This feature allows you to broadcast a file (with content) or a folder (with just links), which is then available for retrieval by other peers (or yourself) until the process is restarted or the in-memory cache is cleared.
Usage Example
// Broadcast a file in-memory
const hash = await peernet.broadcast('/example', {
content: new TextEncoder().encode('hello world')
});
// Broadcast a folder (just links) in-memory
const folderHash = await peernet.broadcast('/folder', {
links: [
{ path: '/file1', hash: 'hash1' },
{ path: '/file2', hash: 'hash2' }
]
});
// Another peer can request the data by hash
// (handled internally by peernet.handleData)Detailed Example
// 1. Broadcast a file in-memory
const hash = await peernet.broadcast('/example', {
content: new TextEncoder().encode('Hello Peernet!')
});
console.log('Broadcasted hash:', hash);
// 2. Broadcast a folder in-memory
const folderHash = await peernet.broadcast('/folder', {
links: [
{ path: '/file1', hash: 'hash1' },
{ path: '/file2', hash: 'hash2' }
]
});
console.log('Broadcasted folder hash:', folderHash);
// 3. Simulate a peer requesting the data by hash
const mockPeer = {
connected: true,
send: async (data, id) => {
// Handle the received data (for demo/testing)
const DataResponseProto = globalThis.peernet.protos['peernet-data-response'];
const decodedProto = await new DataResponseProto(data);
await decodedProto.decode();
if (decodedProto.decoded.data) {
const decodedContent = new TextDecoder().decode(decodedProto.decoded.data);
console.log('Received content:', decodedContent);
} else if (decodedProto.decoded.links) {
console.log('Received folder links:', decodedProto.decoded.links);
}
}
};
const proto = { decoded: { hash } };
await peernet.handleData(mockPeer, 'test-id', proto);Data Flow Diagram
[You/Peer] --(broadcast)--> [Peernet Node: In-Memory Map]
^ |
| |
+------(handleData request)--------+broadcast()stores the data in-memory and returns a hash.handleData()retrieves the data by hash and sends it to the requesting peer.The broadcasted data is stored in-memory and is not persisted to disk.
Retrieval is by hash, matching the value returned from
broadcast().The feature is useful for ephemeral data, rapid prototyping, or testing.
If the process restarts, the in-memory data is lost.
Testing
Automated tests verify that broadcasting and retrieving in-memory data works as expected, including content integrity and hash matching.
Folder Support (Files with Just Links)
Peernet supports "folders" as special file objects that contain only links to other files, without any content. This allows you to represent directory structures or collections of files.
Example: Broadcasting a Folder
const folderHash = await peernet.broadcast('/folder', {
links: [
{ path: '/file1', hash: 'hash1' },
{ path: '/file2', hash: 'hash2' }
]
});Retrieving a Folder
When a folder is retrieved (e.g., via handleData), its links array will be preserved, and content will be empty or undefined.
Test Coverage Automated tests verify that broadcasting and retrieving folders works as expected, ensuring the links are preserved and content is empty or undefined.
Development
Note: You need to install jsproject
npm i -g @vandeurenglenn/projectScripts
- Watch:
npm run watch - Build:
npm run build - Test:
npm test - Serve HTML Demo:
npm run demo
License
MIT
