Compare commits

...

2 Commits

Author SHA1 Message Date
Anselm
48279ed642 Clearer API 2025-06-23 10:09:27 +01:00
Anselm
88dbfefd17 Initial interface draft 2025-06-12 19:42:27 +01:00
3 changed files with 99 additions and 145 deletions

View File

@@ -369,11 +369,9 @@ export class CoValueCore {
tryAddTransactions(
sessionID: SessionID,
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
notifyMode: "immediate" | "deferred",
skipVerify: boolean = false,
givenNewStreamingHash?: StreamingHash,
): Result<true, TryAddTransactionsError> {
return this.node
.resolveAccountAgent(
@@ -390,14 +388,12 @@ export class CoValueCore {
const signerID = this.crypto.getAgentSignerID(agent);
const result = this.verified.tryAddTransactions(
const result = this.verified.tryAdd(
sessionID,
signerID,
newTransactions,
givenExpectedNewHash,
newSignature,
skipVerify,
givenNewStreamingHash,
);
if (result.isOk()) {
@@ -531,30 +527,33 @@ export class CoValueCore {
) as SessionID)
: this.node.currentSessionID;
const { expectedNewHash, newStreamingHash } =
this.verified.expectedNewHashAfter(sessionID, [transaction]);
const agent = this.node.getCurrentAgent();
const signature = this.crypto.sign(
this.node.getCurrentAgent().currentSignerSecret(),
expectedNewHash,
this.verified.addNew(
sessionID,
agent.currentSignerID(),
[transaction],
agent.currentSignerSecret(),
);
const success = this.tryAddTransactions(
sessionID,
[transaction],
expectedNewHash,
signature,
"immediate",
true,
newStreamingHash,
)._unsafeUnwrap({ withStackTrace: true });
this.node.syncManager.recordTransactionsSize([transaction], "local");
void this.node.syncManager.requestCoValueSync(this);
if (success) {
this.node.syncManager.recordTransactionsSize([transaction], "local");
void this.node.syncManager.requestCoValueSync(this);
if (
this._cachedContent &&
"processNewTransactions" in this._cachedContent &&
typeof this._cachedContent.processNewTransactions === "function"
) {
this._cachedContent.processNewTransactions();
} else {
this._cachedContent = undefined;
}
return success;
this._cachedDependentOn = undefined;
this.notifyUpdate("immediate");
return true;
}
getCurrentContent(options?: {

View File

@@ -1,12 +1,16 @@
import { Result, err, ok } from "neverthrow";
import { AnyRawCoValue } from "../coValue.js";
import {
APPEND_INVALID_SIGNATURE,
APPEND_OK,
AppendOnlyVerifiedLog,
CryptoProvider,
Encrypted,
Hash,
KeyID,
Signature,
SignerID,
SignerSecret,
StreamingHash,
} from "../crypto/crypto.js";
import { RawCoID, SessionID, TransactionID } from "../ids.js";
@@ -48,13 +52,15 @@ export type TrustingTransaction = {
export type Transaction = PrivateTransaction | TrustingTransaction;
type SessionLog = {
readonly transactions: Transaction[];
lastHash?: Hash;
streamingHash: StreamingHash;
readonly signatureAfter: { [txIdx: number]: Signature | undefined };
lastSignature: Signature;
};
// type SessionLog = {
// readonly transactions: Transaction[];
// lastHash?: Hash;
// streamingHash: StreamingHash;
// readonly signatureAfter: { [txIdx: number]: Signature | undefined };
// lastSignature: Signature;
// };
type SessionLog = AppendOnlyVerifiedLog<Transaction>;
export type ValidatedSessions = Map<SessionID, SessionLog>;
@@ -82,134 +88,47 @@ export class VerifiedState {
// do a deep clone, including the sessions
const clonedSessions = new Map();
for (let [sessionID, sessionLog] of this.sessions) {
clonedSessions.set(sessionID, {
lastSignature: sessionLog.lastSignature,
lastHash: sessionLog.lastHash,
streamingHash: sessionLog.streamingHash.clone(),
signatureAfter: { ...sessionLog.signatureAfter },
transactions: sessionLog.transactions.slice(),
} satisfies SessionLog);
clonedSessions.set(sessionID, sessionLog.clone());
}
return new VerifiedState(this.id, this.crypto, this.header, clonedSessions);
}
tryAddTransactions(
addNew(
sessionID: SessionID,
signerID: SignerID,
newTransactions: Transaction[],
signerSecret: SignerSecret,
) {
const sessionLog =
this.sessions.get(sessionID) ||
this.crypto.emptyAppendOnlyVerifiedLog(signerID);
sessionLog.addNew(newTransactions, signerSecret);
this.sessions.set(sessionID, sessionLog);
}
tryAdd(
sessionID: SessionID,
signerID: SignerID,
newTransactions: Transaction[],
givenExpectedNewHash: Hash | undefined,
newSignature: Signature,
skipVerify: boolean = false,
givenNewStreamingHash?: StreamingHash,
): Result<true, TryAddTransactionsError> {
if (skipVerify === true && givenNewStreamingHash && givenExpectedNewHash) {
this.doAddTransactions(
sessionID,
newTransactions,
newSignature,
givenExpectedNewHash,
givenNewStreamingHash,
);
const sessionLog =
this.sessions.get(sessionID) ||
this.crypto.emptyAppendOnlyVerifiedLog(signerID);
const result = sessionLog.tryAdd(newTransactions, newSignature, skipVerify);
if (result === APPEND_OK) {
this.sessions.set(sessionID, sessionLog);
return ok(true as const);
} else {
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
sessionID,
newTransactions,
);
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
return err({
type: "InvalidHash",
id: this.id,
expectedNewHash,
givenExpectedNewHash,
} satisfies InvalidHashError);
}
if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
sessionID,
signerID,
} satisfies InvalidSignatureError);
}
this.doAddTransactions(
sessionID,
newTransactions,
return err({
type: "InvalidSignature",
id: this.id,
newSignature,
expectedNewHash,
newStreamingHash,
);
sessionID,
signerID: sessionLog.signerID,
} satisfies InvalidSignatureError);
}
return ok(true as const);
}
private doAddTransactions(
sessionID: SessionID,
newTransactions: Transaction[],
newSignature: Signature,
expectedNewHash: Hash,
newStreamingHash: StreamingHash,
) {
const transactions = this.sessions.get(sessionID)?.transactions ?? [];
for (const tx of newTransactions) {
transactions.push(tx);
}
const signatureAfter = this.sessions.get(sessionID)?.signatureAfter ?? {};
const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
(max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
-1,
);
const sizeOfTxsSinceLastInbetweenSignature = transactions
.slice(lastInbetweenSignatureIdx + 1)
.reduce(
(sum, tx) =>
sum +
(tx.privacy === "private"
? tx.encryptedChanges.length
: tx.changes.length),
0,
);
if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
signatureAfter[transactions.length - 1] = newSignature;
}
this.sessions.set(sessionID, {
transactions,
lastHash: expectedNewHash,
streamingHash: newStreamingHash,
lastSignature: newSignature,
signatureAfter: signatureAfter,
});
this._cachedNewContentSinceEmpty = undefined;
this._cachedKnownState = undefined;
}
expectedNewHashAfter(
sessionID: SessionID,
newTransactions: Transaction[],
): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
const streamingHash =
this.sessions.get(sessionID)?.streamingHash.clone() ??
new StreamingHash(this.crypto);
for (const transaction of newTransactions) {
streamingHash.update(transaction);
}
return {
expectedNewHash: streamingHash.digest(),
newStreamingHash: streamingHash,
};
}
newContentSince(

View File

@@ -1,4 +1,5 @@
import { base58 } from "@scure/base";
import { Transaction } from "../coValueCore/verifiedState.js";
import { RawAccountID } from "../coValues/account.js";
import { AgentID, RawCoID, TransactionID } from "../ids.js";
import { SessionID } from "../ids.js";
@@ -297,6 +298,10 @@ export abstract class CryptoProvider<Blake3State = any> {
newRandomSessionID(accountID: RawAccountID | AgentID): SessionID {
return `${accountID}_session_z${base58.encode(this.randomBytes(8))}`;
}
abstract emptyAppendOnlyVerifiedLog<T>(
signedID: SignerID,
): AppendOnlyVerifiedLog<T>;
}
export type Hash = `hash_z${string}`;
@@ -341,3 +346,34 @@ export type KeySecret = `keySecret_z${string}`;
export type KeyID = `key_z${string}`;
export const secretSeedLength = 32;
export const APPEND_OK = 0;
export const APPEND_INVALID_SIGNATURE = 1;
export type AppendResult = typeof APPEND_OK | typeof APPEND_INVALID_SIGNATURE;
export interface SessionLog {
// these only have to be maintained in JS, anything lower level
// only needs to store the encoded items and last streaming hash
// and return the new signature on addNew
transactions: readonly Transaction[];
signerID: SignerID;
lastSignature: Signature;
signatureAfter: { [txIdx: number]: Signature | undefined };
clone(): SessionLog;
tryAdd(
transactions: Transaction[],
newSignature: Signature,
skipVerify: boolean,
): AppendResult;
addNewTransaction(changes: JsonValue[], signerSecret: SignerSecret): void;
// note: this may need to decrypt all transactions since the last decrypted one, even if some of these are invalid
// in case we use compression etc.
// invariant: we must call this with strictly monotonically increasing txIndex
decryptNextTransactionChanges(
txIndex: number,
keySecret: KeySecret,
): JsonValue[] | undefined;
}