activeledger 1.0.0
activeledger: ^1.0.0 copied to clipboard
Activeledger SDK for Dart and Flutter. Onboard identities, sign payloads, and sign and submit transactions with secp256k1 and post-quantum ML-DSA-65 keys.
Activeledger SDK for Dart #
Dart and Flutter SDK for Activeledger, with post-quantum identity support.
Requires Activeledger 4.7.0+ for ml-dsa-65 and falcon-512. Dart 3.10+.
A port of the JavaScript SDK: the
same handlers (KeyHandler, TransactionHandler, PayloadHandler,
Connection), the same key file format, and byte-for-byte the
same signatures, checked against the JavaScript SDK's cross-language vectors
and against a real four-node network.
Install #
dart pub add activeledger
For Falcon-512 as well, add the add-on. Its native library is downloaded and bundled automatically for Android, iOS, macOS, Linux and Windows:
dart pub add activeledger_falcon
Quick start #
import 'package:activeledger/activeledger.dart';
final ledger = Activeledger('http://localhost:5260');
final key = ledger.generateKey('identity', type: KeyType.mlDsa65);
await ledger.onboard(key);
print(key.identity); // the new identity's stream id
With the Falcon add-on:
import 'package:activeledger_falcon/activeledger_falcon.dart';
ActiveledgerFalcon.enable(); // once, at startup
final key = ledger.generateKey('identity', type: KeyType.preferredPostQuantum);
KeyType.preferredPostQuantum is Falcon-512 when the add-on is enabled and
ML-DSA-65 otherwise, so the same code runs either way.
Key types #
| Key type | Wire string | Public | Private | Signature | Encoding |
|---|---|---|---|---|---|
| secp256k1 | secp256k1 |
33 or 65 | 32 | ~70-72, variable DER | 0x hex |
| ML-DSA-65 | ml-dsa-65 |
1952 | 4032 | 3309 | base64 |
| Falcon-512 | falcon-512 |
897 | 1281 | 649-662, variable | base64 |
| RSA (legacy) | rsa |
PEM | PEM | 256 for 2048-bit | PEM |
Use secp256k1 unless the identity must outlive a cryptographically relevant quantum computer: roughly 22x smaller per transaction than ML-DSA-65, and every byte is stored on the ledger permanently and replicated to every node. Between the post-quantum schemes, Falcon-512 signatures are about a fifth the size of ML-DSA-65's; ML-DSA-65 is the finalised standard and needs no native code.
final keys = KeyHandler();
final ec = keys.generateKey('me'); // secp256k1, uncompressed
final ec33 = keys.generateKey('me', compressed: true); // 33-byte public key
final pq = keys.generateKey('me', type: KeyType.mlDsa65);
ec.key.publicKey; // "0x04a1b2..." - give this to the ledger
ec.key.privateKey; // keep this
- secp256k1 keys are
0x-prefixed hex; post-quantum keys are base64 of the raw algorithm bytes. The prefix is required, not tolerated. - secp256k1 signing is RFC 6979 deterministic and low-S, so its signatures
are byte-identical to
@noble/curvesfor the same key and message. Verification accepts high-S, because the ledger verifies through OpenSSL and produces high-S freely. - Post-quantum signing is hedged: two signatures over one message differ, and both verify. Falcon signature length varies - never assume it fixed.
- RSA keys (a network's contract deployer, say) sign and verify from PEM. They cannot be generated. secp256k1 keys exported as PEM by older SDKs work too.
KeyType.fromWireparsesbitcoinandethereumas secp256k1, because the ledger routes them to identical verification. They are never emitted.
Seeds and recovery phrases #
final keys = KeyHandler();
final key = keys.generateBip39Key('me', type: KeyType.mlDsa65);
print(key.phrase); // 12 words - store them
final same = keys.restoreBip39Key('me', key.phrase!, type: KeyType.mlDsa65);
final withPass = keys.restoreBip39Key('me', phrase, passphrase: 'extra words');
final fromSeed = keys.generateKeyFromSeed('me', seed32, type: KeyType.mlDsa65);
The same seed gives the same identity in every Activeledger SDK, which makes a seed the portable private-key format. One phrase can back a secp256k1, an ml-dsa-65 and a falcon-512 identity at once, each derived independently:
| Type | Seed from the BIP-39 seed S |
|---|---|
secp256k1 |
HMAC-SHA512("Bitcoin seed", S)[0..32] |
ml-dsa-65 |
HKDF-SHA512(S, salt="", info="activeledger-seed-v1:ml-dsa-65", 32) |
falcon-512 |
HKDF-SHA512(S, salt="", info="activeledger-seed-v1:falcon-512", 48) |
Recovery exposes each step - validate, toSeed, deriveSeed,
deriveBip32MasterKey, generateMnemonic - so you can see which one differs
when a recovered identity is not the expected one.
- A seed of the wrong length is refused, not padded: a padded seed is a different identity, not a malformed one.
- For secp256k1 the seed is the private scalar, so a seed of zero or at or above the curve order is refused rather than reduced mod n.
- Phrases are validated, wordlist and checksum. A mistyped phrase that is
not checked derives a valid key for an identity nobody owns. Pass
validate: falseonly to derive from a string that is not a mnemonic. legacy: truerecovers a phrase made by the old@activeledger/sdk-bip39package (SHA256 of the phrase). Recovery only, never for new keys.
Transactions #
final transactions = TransactionHandler();
final tx = transactions.labelledTransaction(
key: key, // must be onboarded
namespace: 'mynamespace',
contract: 'mycontract',
inputLabel: 'input',
stream: key.identity!,
inputData: {'message': 'hello'},
outputs: {'target-stream': {'amount': 10}},
entry: 'update', // optional $entry
);
final response = await connection.sendTransaction(tx);
if (!response.committed) throw Exception(response.errors);
response.streams.created; // streams created
response.responses; // returnToRemote values
For any other shape - several inputs, several signers - build the $tx
yourself and sign it once per key:
final tx = Transaction(tx: {
r'$namespace': 'ns',
r'$contract': 'c',
r'$i': {alice.identity!: {}, bob.identity!: {}},
});
transactions.signTransaction(tx, alice);
transactions.signTransaction(tx, bob);
$sigs is keyed by identity, or by $i label for a self-signed
transaction (selfsign: true), which is what the ledger looks up.
Reading state #
There is no separate read API: a node's storage service listens only on the
node's own host. Name the streams in readonly ($r) and have the contract
return values with returnToRemote; they arrive in response.responses.
Events #
This SDK deliberately has no events client. A node serves contract events
from its own database at http://localhost:<storage port>/activeledgerevents/events
(a server-sent event stream), and that service should never be exposed
beyond the node's host - so a client such as a mobile app cannot, and
should not, connect to it.
To react to events, run your own listener on the node's host and relay
what your application needs through your own backend. Each event is an SSE
frame whose id is <milliseconds>-<counter>,<umid> and whose data is
{"name", "data", "phase", "contract"}.
ActiveCore, which previously served events and activity feeds, is deprecated and should not be used.
Signing payloads #
final payloads = PayloadHandler();
final signature = payloads.sign({'pair': 'VNR/USDT', 'side': 'sell'}, key);
payloads.verify(order, signature, publicKey, type: KeyType.mlDsa65); // bool
payloads.canonical(order) is the exact string signed. It is key-order
sensitive, so persist it alongside the signature and verify that.
Key files #
await keys.exportKey(key, 'keys', createDir: true); // keys/<name>.json
final loaded = await keys.importKey('keys/identity.json');
The format is the JavaScript SDK's, so key files move between the two.
Signing elsewhere #
Every handler takes a CryptoProvider. Implement one to sign in an HSM or
secure enclave, or register a custom PostQuantumScheme with
PostQuantum.register.
Things that will bite you #
Always send the key type. The ledger defaults a missing type to rsa.
This SDK always sends it; if you hand-build an envelope, do the same.
A rejected transaction is HTTP 200. Check response.committed, never the
status code.
Errors are unhelpful by design. A wrong type string, a wrong-length key or signed bytes differing by one escape all come back as 1220 "Signature Incorrect". This SDK validates key lengths and type strings up front so these fail locally with a message naming the problem.
Signatures cover $tx only, not the envelope. tx.signedString is exactly
what was signed, which is the fastest way to diagnose a 1220.
Canonical JSON #
Signatures cover the exact bytes of JavaScript's JSON.stringify($tx). Dart's
jsonEncode differs from it on numbers - 1.0 versus 1, 1e20 versus
100000000000000000000.0 - so canonicalJson reproduces JSON.stringify
exactly. It is checked against the cross-language number vectors and against
Node on tens of thousands of random doubles. It refuses NaN, infinities and
integers a JavaScript number cannot hold, rather than signing something you
did not write.
Testing #
dart test
Integration tests run against a real network. From an activeledger
checkout, npm run test:network:serve, then:
AL_NODES=http://127.0.0.1:5510 AL_STORAGE=http://127.0.0.1:5509 dart test test/integration
Licence #
MIT