mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
message integrity
This commit is contained in:
parent
490051a650
commit
fd63a0d5e0
@ -1,145 +1,191 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../proto/proto.dart' as proto;
|
import '../../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
import 'author_input_source.dart';
|
import 'author_input_source.dart';
|
||||||
|
import 'message_integrity.dart';
|
||||||
import 'output_position.dart';
|
import 'output_position.dart';
|
||||||
|
|
||||||
class AuthorInputQueue {
|
class AuthorInputQueue {
|
||||||
AuthorInputQueue({
|
AuthorInputQueue._({
|
||||||
required this.author,
|
required TypedKey author,
|
||||||
required this.inputSource,
|
required AuthorInputSource inputSource,
|
||||||
required this.lastOutputPosition,
|
required OutputPosition? outputPosition,
|
||||||
required this.onError,
|
required void Function(Object, StackTrace?) onError,
|
||||||
}):
|
required MessageIntegrity messageIntegrity,
|
||||||
assert(inputSource.messages.count>0, 'no input source window length'),
|
}) : _author = author,
|
||||||
assert(inputSource.messages.elements.isNotEmpty, 'no input source elements'),
|
_onError = onError,
|
||||||
assert(inputSource.messages.tail >= inputSource.messages.elements.length, 'tail is before initial messages end'),
|
_inputSource = inputSource,
|
||||||
assert(inputSource.messages.tail > 0, 'tail is not greater than zero'),
|
_outputPosition = outputPosition,
|
||||||
currentPosition = inputSource.messages.tail,
|
_lastMessage = outputPosition?.message,
|
||||||
currentWindow = inputSource.messages.elements,
|
_messageIntegrity = messageIntegrity,
|
||||||
windowLength = inputSource.messages.count,
|
_currentPosition = inputSource.currentWindow.last;
|
||||||
windowFirst = inputSource.messages.tail - inputSource.messages.elements.length,
|
|
||||||
windowLast = inputSource.messages.tail - 1;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
static Future<AuthorInputQueue?> create({
|
||||||
|
required TypedKey author,
|
||||||
bool get isEmpty => toReconcile.isEmpty;
|
required AuthorInputSource inputSource,
|
||||||
|
required OutputPosition? outputPosition,
|
||||||
proto.Message? get current => toReconcile.firstOrNull;
|
required void Function(Object, StackTrace?) onError,
|
||||||
|
}) async {
|
||||||
bool consume() {
|
final queue = AuthorInputQueue._(
|
||||||
toReconcile.removeFirst();
|
author: author,
|
||||||
return toReconcile.isNotEmpty;
|
inputSource: inputSource,
|
||||||
|
outputPosition: outputPosition,
|
||||||
|
onError: onError,
|
||||||
|
messageIntegrity: await MessageIntegrity.create(author: author));
|
||||||
|
if (!await queue._findStartOfWork()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> prepareInputQueue() async {
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Go through batches of the input dhtlog starting with
|
// Public interface
|
||||||
// the current cubit state which is at the tail of the log
|
|
||||||
// Find the last reconciled message for this author
|
|
||||||
|
|
||||||
outer:
|
// Check if there are no messages in this queue to reconcile
|
||||||
|
bool get isEmpty => _currentMessage == null;
|
||||||
|
|
||||||
|
// Get the current message that needs reconciliation
|
||||||
|
proto.Message? get current => _currentMessage;
|
||||||
|
|
||||||
|
// Get the earliest output position to start inserting
|
||||||
|
OutputPosition? get outputPosition => _outputPosition;
|
||||||
|
|
||||||
|
// Get the author of this queue
|
||||||
|
TypedKey get author => _author;
|
||||||
|
|
||||||
|
// Remove a reconciled message and move to the next message
|
||||||
|
// Returns true if there is more work to do
|
||||||
|
Future<bool> consume() async {
|
||||||
while (true) {
|
while (true) {
|
||||||
for (var rn = currentWindow.length;
|
_lastMessage = _currentMessage;
|
||||||
rn >= 0 && currentPosition >= 0;
|
|
||||||
rn--, currentPosition--) {
|
|
||||||
final elem = currentWindow[rn];
|
|
||||||
|
|
||||||
// If we've found an input element that is older than our last
|
_currentPosition++;
|
||||||
// reconciled message for this author, then we stop
|
|
||||||
if (lastOutputPosition != null) {
|
|
||||||
if (elem.value.timestamp < lastOutputPosition!.message.timestamp) {
|
|
||||||
break outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop the 'offline' elements because we don't reconcile
|
// Get more window if we need to
|
||||||
// anything until it has been confirmed to be committed to the DHT
|
if (!await _updateWindow()) {
|
||||||
if (elem.isOffline) {
|
// Window is not available so this queue can't work right now
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final nextMessage = _inputSource.currentWindow
|
||||||
|
.elements[_currentPosition - _inputSource.currentWindow.first];
|
||||||
|
|
||||||
|
// Drop the 'offline' elements because we don't reconcile
|
||||||
|
// anything until it has been confirmed to be committed to the DHT
|
||||||
|
if (nextMessage.isOffline) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastMessage != null) {
|
||||||
|
// Ensure the timestamp is not moving backward
|
||||||
|
if (nextMessage.value.timestamp < _lastMessage!.timestamp) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to head of reconciliation queue
|
|
||||||
toReconcile.addFirst(elem.value);
|
|
||||||
if (toReconcile.length > _maxQueueChunk) {
|
|
||||||
toReconcile.removeLast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentPosition < 0) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xxx update window here and make this and other methods work
|
// Verify the id chain for the message
|
||||||
|
final matchId = await _messageIntegrity.generateMessageId(_lastMessage);
|
||||||
|
if (matchId.compare(nextMessage.value.idBytes) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature for the message
|
||||||
|
if (!await _messageIntegrity.verifyMessage(nextMessage.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentMessage = nextMessage.value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Internal implementation
|
||||||
|
|
||||||
|
// Walk backward from the tail of the input queue to find the first
|
||||||
|
// message newer than our last reconcicled message from this author
|
||||||
|
// Returns false if no work is needed
|
||||||
|
Future<bool> _findStartOfWork() async {
|
||||||
|
// Iterate windows over the inputSource
|
||||||
|
outer:
|
||||||
|
while (true) {
|
||||||
|
// Iterate through current window backward
|
||||||
|
for (var i = _inputSource.currentWindow.elements.length;
|
||||||
|
i >= 0 && _currentPosition >= 0;
|
||||||
|
i--, _currentPosition--) {
|
||||||
|
final elem = _inputSource.currentWindow.elements[i];
|
||||||
|
|
||||||
|
// If we've found an input element that is older than our last
|
||||||
|
// reconciled message for this author, then we stop
|
||||||
|
if (_lastMessage != null) {
|
||||||
|
if (elem.value.timestamp < _lastMessage!.timestamp) {
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we're at the beginning of the inputSource then we stop
|
||||||
|
if (_currentPosition < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get more window if we need to
|
||||||
|
if (!await _updateWindow()) {
|
||||||
|
// Window is not available or things are empty so this
|
||||||
|
// queue can't work right now
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current position should be equal to the first message to process
|
||||||
|
// and the current window to process should not be empty
|
||||||
|
return _inputSource.currentWindow.elements.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
// Slide the window toward the current position and load the batch around it
|
// Slide the window toward the current position and load the batch around it
|
||||||
Future<bool> updateWindow() async {
|
Future<bool> _updateWindow() async {
|
||||||
|
// Check if we are still in the window
|
||||||
// Check if we are still in the window
|
if (_currentPosition >= _inputSource.currentWindow.first &&
|
||||||
if (currentPosition>=windowFirst && currentPosition <= windowLast) {
|
_currentPosition <= _inputSource.currentWindow.last) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the length of the cubit
|
|
||||||
final inputLength = await inputSource.cubit.operate((r) async => r.length);
|
|
||||||
|
|
||||||
// If not, slide the window
|
|
||||||
if (currentPosition<windowFirst) {
|
|
||||||
// Slide it backward, current position is now windowLast
|
|
||||||
windowFirst = max((currentPosition - windowLength) + 1, 0);
|
|
||||||
windowLast = currentPosition;
|
|
||||||
} else {
|
|
||||||
// Slide it forward, current position is now windowFirst
|
|
||||||
windowFirst = currentPosition;
|
|
||||||
windowLast = min((currentPosition + windowLength) - 1, inputLength - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get another input batch futher back
|
|
||||||
final nextWindow =
|
|
||||||
await inputSource.cubit.loadElements(windowLast + 1, (windowLast + 1) - windowFirst);
|
|
||||||
final asErr = nextWindow.asError;
|
|
||||||
if (asErr != null) {
|
|
||||||
onError(asErr.error, asErr.stackTrace);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final asLoading = nextWindow.asLoading;
|
|
||||||
if (asLoading != null) {
|
|
||||||
// xxx: no need to block the cubit here for this
|
|
||||||
// xxx: might want to switch to a 'busy' state though
|
|
||||||
// xxx: to let the messages view show a spinner at the bottom
|
|
||||||
// xxx: while we reconcile...
|
|
||||||
// emit(const AsyncValue.loading());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
currentWindow = nextWindow.asData!.value;
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get another input batch futher back
|
||||||
|
final avOk =
|
||||||
|
await _inputSource.updateWindow(_currentPosition, _maxWindowLength);
|
||||||
|
|
||||||
|
final asErr = avOk.asError;
|
||||||
|
if (asErr != null) {
|
||||||
|
_onError(asErr.error, asErr.stackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final asLoading = avOk.asLoading;
|
||||||
|
if (asLoading != null) {
|
||||||
|
// xxx: no need to block the cubit here for this
|
||||||
|
// xxx: might want to switch to a 'busy' state though
|
||||||
|
// xxx: to let the messages view show a spinner at the bottom
|
||||||
|
// xxx: while we reconcile...
|
||||||
|
// emit(const AsyncValue.loading());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return avOk.asData!.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
final TypedKey author;
|
final TypedKey _author;
|
||||||
final ListQueue<proto.Message> toReconcile = ListQueue<proto.Message>();
|
final AuthorInputSource _inputSource;
|
||||||
final AuthorInputSource inputSource;
|
final OutputPosition? _outputPosition;
|
||||||
final OutputPosition? lastOutputPosition;
|
final void Function(Object, StackTrace?) _onError;
|
||||||
final void Function(Object, StackTrace?) onError;
|
final MessageIntegrity _messageIntegrity;
|
||||||
|
|
||||||
|
// The last message we've consumed
|
||||||
|
proto.Message? _lastMessage;
|
||||||
// The current position in the input log that we are looking at
|
// The current position in the input log that we are looking at
|
||||||
int currentPosition;
|
int _currentPosition;
|
||||||
// The current input window elements
|
// The current message we're looking at
|
||||||
IList<DHTLogElementState<proto.Message>> currentWindow;
|
proto.Message? _currentMessage;
|
||||||
// The first position of the sliding input window
|
|
||||||
int windowFirst;
|
|
||||||
// The last position of the sliding input window
|
|
||||||
int windowLast;
|
|
||||||
// Desired maximum window length
|
// Desired maximum window length
|
||||||
int windowLength;
|
static const int _maxWindowLength = 256;
|
||||||
|
|
||||||
static const int _maxQueueChunk = 256;
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,76 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../../proto/proto.dart' as proto;
|
import '../../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
class AuthorInputSource {
|
@immutable
|
||||||
AuthorInputSource({required this.messages, required this.cubit});
|
class InputWindow {
|
||||||
|
const InputWindow(
|
||||||
final DHTLogStateData<proto.Message> messages;
|
{required this.elements, required this.first, required this.last});
|
||||||
final DHTLogCubit<proto.Message> cubit;
|
final IList<OnlineElementState<proto.Message>> elements;
|
||||||
|
final int first;
|
||||||
|
final int last;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthorInputSource {
|
||||||
|
AuthorInputSource.fromCubit(
|
||||||
|
{required DHTLogStateData<proto.Message> cubitState,
|
||||||
|
required this.cubit}) {
|
||||||
|
_currentWindow = InputWindow(
|
||||||
|
elements: cubitState.elements,
|
||||||
|
first: cubitState.tail - cubitState.elements.length,
|
||||||
|
last: cubitState.tail - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
InputWindow get currentWindow => _currentWindow;
|
||||||
|
|
||||||
|
Future<AsyncValue<bool>> updateWindow(
|
||||||
|
int currentPosition, int windowLength) async =>
|
||||||
|
cubit.operate((reader) async {
|
||||||
|
// See if we're beyond the input source
|
||||||
|
if (currentPosition < 0 || currentPosition >= reader.length) {
|
||||||
|
return const AsyncValue.data(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide the window if we need to
|
||||||
|
var first = _currentWindow.first;
|
||||||
|
var last = _currentWindow.last;
|
||||||
|
if (currentPosition < first) {
|
||||||
|
// Slide it backward, current position is now last
|
||||||
|
first = max((currentPosition - windowLength) + 1, 0);
|
||||||
|
last = currentPosition;
|
||||||
|
} else if (currentPosition > last) {
|
||||||
|
// Slide it forward, current position is now first
|
||||||
|
first = currentPosition;
|
||||||
|
last = min((currentPosition + windowLength) - 1, reader.length - 1);
|
||||||
|
} else {
|
||||||
|
return const AsyncValue.data(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get another input batch futher back
|
||||||
|
final nextWindow = await cubit.loadElementsFromReader(
|
||||||
|
reader, last + 1, (last + 1) - first);
|
||||||
|
final asErr = nextWindow.asError;
|
||||||
|
if (asErr != null) {
|
||||||
|
return AsyncValue.error(asErr.error, asErr.stackTrace);
|
||||||
|
}
|
||||||
|
final asLoading = nextWindow.asLoading;
|
||||||
|
if (asLoading != null) {
|
||||||
|
return const AsyncValue.loading();
|
||||||
|
}
|
||||||
|
_currentWindow = InputWindow(
|
||||||
|
elements: nextWindow.asData!.value, first: first, last: last);
|
||||||
|
return const AsyncValue.data(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
final DHTLogCubit<proto.Message> cubit;
|
||||||
|
|
||||||
|
late InputWindow _currentWindow;
|
||||||
}
|
}
|
||||||
|
74
lib/chat/cubits/reconciliation/message_integrity.dart
Normal file
74
lib/chat/cubits/reconciliation/message_integrity.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
import '../../../proto/proto.dart' as proto;
|
||||||
|
|
||||||
|
class MessageIntegrity {
|
||||||
|
MessageIntegrity._({
|
||||||
|
required TypedKey author,
|
||||||
|
required VeilidCryptoSystem crypto,
|
||||||
|
}) : _author = author,
|
||||||
|
_crypto = crypto;
|
||||||
|
static Future<MessageIntegrity> create({required TypedKey author}) async {
|
||||||
|
final crypto = await Veilid.instance.getCryptoSystem(author.kind);
|
||||||
|
return MessageIntegrity._(author: author, crypto: crypto);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public interface
|
||||||
|
|
||||||
|
Future<Uint8List> generateMessageId(proto.Message? previous) async {
|
||||||
|
if (previous == null) {
|
||||||
|
// If there's no last sent message,
|
||||||
|
// we start at a hash of the identity public key
|
||||||
|
return _generateInitialId();
|
||||||
|
} else {
|
||||||
|
// If there is a last message, we generate the hash
|
||||||
|
// of the last message's signature and use it as our next id
|
||||||
|
return _hashSignature(previous.signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signMessage(
|
||||||
|
proto.Message message,
|
||||||
|
SecretKey authorSecret,
|
||||||
|
) async {
|
||||||
|
// Ensure this message is not already signed
|
||||||
|
assert(!message.hasSignature(), 'should not sign message twice');
|
||||||
|
// Generate data to sign
|
||||||
|
final data = Uint8List.fromList(utf8.encode(message.writeToJson()));
|
||||||
|
|
||||||
|
// Sign with our identity
|
||||||
|
final signature = await _crypto.sign(_author.value, authorSecret, data);
|
||||||
|
|
||||||
|
// Add to the message
|
||||||
|
message.signature = signature.toProto();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> verifyMessage(proto.Message message) async {
|
||||||
|
// Ensure the message is signed
|
||||||
|
assert(message.hasSignature(), 'should not verify unsigned message');
|
||||||
|
final signature = message.signature.toVeilid();
|
||||||
|
|
||||||
|
// Generate data to sign
|
||||||
|
final messageNoSig = message.deepCopy()..clearSignature();
|
||||||
|
final data = Uint8List.fromList(utf8.encode(messageNoSig.writeToJson()));
|
||||||
|
|
||||||
|
// Verify signature
|
||||||
|
return _crypto.verify(_author.value, data, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private implementation
|
||||||
|
|
||||||
|
Future<Uint8List> _generateInitialId() async =>
|
||||||
|
(await _crypto.generateHash(_author.decode())).decode();
|
||||||
|
|
||||||
|
Future<Uint8List> _hashSignature(proto.Signature signature) async =>
|
||||||
|
(await _crypto.generateHash(signature.toVeilid().decode())).decode();
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
final TypedKey _author;
|
||||||
|
final VeilidCryptoSystem _crypto;
|
||||||
|
}
|
@ -21,14 +21,14 @@ class MessageReconciliation {
|
|||||||
|
|
||||||
void reconcileMessages(
|
void reconcileMessages(
|
||||||
TypedKey author,
|
TypedKey author,
|
||||||
DHTLogStateData<proto.Message> inputMessages,
|
DHTLogStateData<proto.Message> inputMessagesCubitState,
|
||||||
DHTLogCubit<proto.Message> inputMessagesCubit) {
|
DHTLogCubit<proto.Message> inputMessagesCubit) {
|
||||||
if (inputMessages.elements.isEmpty) {
|
if (inputMessagesCubitState.elements.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_inputSources[author] =
|
_inputSources[author] = AuthorInputSource.fromCubit(
|
||||||
AuthorInputSource(messages: inputMessages, cubit: inputMessagesCubit);
|
cubitState: inputMessagesCubitState, cubit: inputMessagesCubit);
|
||||||
|
|
||||||
singleFuture(this, onError: _onError, () async {
|
singleFuture(this, onError: _onError, () async {
|
||||||
// Take entire list of input sources we have currently and process them
|
// Take entire list of input sources we have currently and process them
|
||||||
@ -63,14 +63,15 @@ class MessageReconciliation {
|
|||||||
{required TypedKey author,
|
{required TypedKey author,
|
||||||
required AuthorInputSource inputSource}) async {
|
required AuthorInputSource inputSource}) async {
|
||||||
// Get the position of our most recent reconciled message from this author
|
// Get the position of our most recent reconciled message from this author
|
||||||
final lastReconciledMessage =
|
final outputPosition = await _findLastOutputPosition(author: author);
|
||||||
await _findNewestReconciledMessage(author: author);
|
|
||||||
|
|
||||||
// Find oldest message we have not yet reconciled
|
// Find oldest message we have not yet reconciled
|
||||||
final inputQueue = await _buildAuthorInputQueue(
|
final inputQueue = await AuthorInputQueue.create(
|
||||||
author: author,
|
author: author,
|
||||||
inputSource: inputSource,
|
inputSource: inputSource,
|
||||||
lastOutputPosition: lastReconciledMessage);
|
outputPosition: outputPosition,
|
||||||
|
onError: _onError,
|
||||||
|
);
|
||||||
return inputQueue;
|
return inputQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ class MessageReconciliation {
|
|||||||
// reconciled message from this author
|
// reconciled message from this author
|
||||||
// XXX: For a group chat, this should find when the author
|
// XXX: For a group chat, this should find when the author
|
||||||
// was added to the membership so we don't just go back in time forever
|
// was added to the membership so we don't just go back in time forever
|
||||||
Future<OutputPosition?> _findNewestReconciledMessage(
|
Future<OutputPosition?> _findLastOutputPosition(
|
||||||
{required TypedKey author}) async =>
|
{required TypedKey author}) async =>
|
||||||
_outputCubit.operate((arr) async {
|
_outputCubit.operate((arr) async {
|
||||||
var pos = arr.length - 1;
|
var pos = arr.length - 1;
|
||||||
@ -95,26 +96,6 @@ class MessageReconciliation {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find oldest message we have not yet reconciled and build a queue forward
|
|
||||||
// from that position
|
|
||||||
Future<AuthorInputQueue?> _buildAuthorInputQueue(
|
|
||||||
{required TypedKey author,
|
|
||||||
required AuthorInputSource inputSource,
|
|
||||||
required OutputPosition? lastOutputPosition}) async {
|
|
||||||
// Make an author input queue
|
|
||||||
final authorInputQueue = AuthorInputQueue(
|
|
||||||
author: author,
|
|
||||||
inputSource: inputSource,
|
|
||||||
lastOutputPosition: lastOutputPosition,
|
|
||||||
onError: _onError);
|
|
||||||
|
|
||||||
if (!await authorInputQueue.prepareInputQueue()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return authorInputQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a list of author input queues and insert their messages
|
// Process a list of author input queues and insert their messages
|
||||||
// into the output array, performing validation steps along the way
|
// into the output array, performing validation steps along the way
|
||||||
Future<void> _reconcileInputQueues({
|
Future<void> _reconcileInputQueues({
|
||||||
@ -130,8 +111,8 @@ class MessageReconciliation {
|
|||||||
// Sort queues from earliest to latest and then by author
|
// Sort queues from earliest to latest and then by author
|
||||||
// to ensure a deterministic insert order
|
// to ensure a deterministic insert order
|
||||||
inputQueues.sort((a, b) {
|
inputQueues.sort((a, b) {
|
||||||
final acmp = a.lastOutputPosition?.pos ?? -1;
|
final acmp = a.outputPosition?.pos ?? -1;
|
||||||
final bcmp = b.lastOutputPosition?.pos ?? -1;
|
final bcmp = b.outputPosition?.pos ?? -1;
|
||||||
if (acmp == bcmp) {
|
if (acmp == bcmp) {
|
||||||
return a.author.toString().compareTo(b.author.toString());
|
return a.author.toString().compareTo(b.author.toString());
|
||||||
}
|
}
|
||||||
@ -139,7 +120,7 @@ class MessageReconciliation {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start at the earliest position we know about in all the queues
|
// Start at the earliest position we know about in all the queues
|
||||||
final firstOutputPos = inputQueues.first.lastOutputPosition?.pos;
|
final firstOutputPos = inputQueues.first.outputPosition?.pos;
|
||||||
// Get the timestamp for this output position
|
// Get the timestamp for this output position
|
||||||
var currentOutputMessage = firstOutputPos == null
|
var currentOutputMessage = firstOutputPos == null
|
||||||
? null
|
? null
|
||||||
@ -167,7 +148,7 @@ class MessageReconciliation {
|
|||||||
added = true;
|
added = true;
|
||||||
|
|
||||||
// Advance this queue
|
// Advance this queue
|
||||||
if (!inputQueue.consume()) {
|
if (!await inputQueue.consume()) {
|
||||||
// Queue is empty now, run a queue purge
|
// Queue is empty now, run a queue purge
|
||||||
someQueueEmpty = true;
|
someQueueEmpty = true;
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export 'message_integrity.dart';
|
||||||
export 'message_reconciliation.dart';
|
export 'message_reconciliation.dart';
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:async_tools/async_tools.dart';
|
import 'package:async_tools/async_tools.dart';
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||||
@ -10,7 +8,7 @@ import 'package:veilid_support/veilid_support.dart';
|
|||||||
import '../../account_manager/account_manager.dart';
|
import '../../account_manager/account_manager.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import 'message_reconciliation.dart';
|
import 'reconciliation/reconciliation.dart';
|
||||||
|
|
||||||
class RenderStateElement {
|
class RenderStateElement {
|
||||||
RenderStateElement(
|
RenderStateElement(
|
||||||
@ -102,12 +100,11 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
|
|
||||||
// Make crypto
|
// Make crypto
|
||||||
Future<void> _initCrypto() async {
|
Future<void> _initCrypto() async {
|
||||||
_messagesCrypto = await _activeAccountInfo
|
_conversationCrypto = await _activeAccountInfo
|
||||||
.makeConversationCrypto(_remoteIdentityPublicKey);
|
.makeConversationCrypto(_remoteIdentityPublicKey);
|
||||||
_localMessagesCryptoSystem =
|
_senderMessageIntegrity = await MessageIntegrity.create(
|
||||||
await Veilid.instance.getCryptoSystem(_localMessagesRecordKey.kind);
|
author: _activeAccountInfo.localAccount.identityMaster
|
||||||
_identityCryptoSystem =
|
.identityPublicTypedKey());
|
||||||
await _activeAccountInfo.localAccount.identityMaster.identityCrypto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open local messages key
|
// Open local messages key
|
||||||
@ -119,7 +116,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
|
debugName: 'SingleContactMessagesCubit::_initSentMessagesCubit::'
|
||||||
'SentMessages',
|
'SentMessages',
|
||||||
parent: _localConversationRecordKey,
|
parent: _localConversationRecordKey,
|
||||||
crypto: _messagesCrypto),
|
crypto: _conversationCrypto),
|
||||||
decodeElement: proto.Message.fromBuffer);
|
decodeElement: proto.Message.fromBuffer);
|
||||||
_sentSubscription =
|
_sentSubscription =
|
||||||
_sentMessagesCubit!.stream.listen(_updateSentMessagesState);
|
_sentMessagesCubit!.stream.listen(_updateSentMessagesState);
|
||||||
@ -133,7 +130,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
|
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
|
||||||
'RcvdMessages',
|
'RcvdMessages',
|
||||||
parent: _remoteConversationRecordKey,
|
parent: _remoteConversationRecordKey,
|
||||||
crypto: _messagesCrypto),
|
crypto: _conversationCrypto),
|
||||||
decodeElement: proto.Message.fromBuffer);
|
decodeElement: proto.Message.fromBuffer);
|
||||||
_rcvdSubscription =
|
_rcvdSubscription =
|
||||||
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
|
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
|
||||||
@ -222,31 +219,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
_renderState();
|
_renderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> _hashSignature(proto.Signature signature) async =>
|
|
||||||
(await _localMessagesCryptoSystem
|
|
||||||
.generateHash(signature.toVeilid().decode()))
|
|
||||||
.decode();
|
|
||||||
|
|
||||||
Future<void> _signMessage(proto.Message message) async {
|
|
||||||
// Generate data to sign
|
|
||||||
final data = Uint8List.fromList(utf8.encode(message.writeToJson()));
|
|
||||||
|
|
||||||
// Sign with our identity
|
|
||||||
final signature = await _identityCryptoSystem.sign(
|
|
||||||
_activeAccountInfo.localAccount.identityMaster.identityPublicKey,
|
|
||||||
_activeAccountInfo.userLogin.identitySecret.value,
|
|
||||||
data);
|
|
||||||
|
|
||||||
// Add to the message
|
|
||||||
message.signature = signature.toProto();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uint8List> _generateInitialId(
|
|
||||||
{required PublicKey identityPublicKey}) async =>
|
|
||||||
(await _localMessagesCryptoSystem
|
|
||||||
.generateHash(identityPublicKey.decode()))
|
|
||||||
.decode();
|
|
||||||
|
|
||||||
Future<void> _processMessageToSend(
|
Future<void> _processMessageToSend(
|
||||||
proto.Message message, proto.Message? previousMessage) async {
|
proto.Message message, proto.Message? previousMessage) async {
|
||||||
// Get the previous message if we don't have one
|
// Get the previous message if we don't have one
|
||||||
@ -255,20 +227,12 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
? null
|
? null
|
||||||
: await r.getProtobuf(proto.Message.fromBuffer, r.length - 1));
|
: await r.getProtobuf(proto.Message.fromBuffer, r.length - 1));
|
||||||
|
|
||||||
if (previousMessage == null) {
|
message.id =
|
||||||
// If there's no last sent message,
|
await _senderMessageIntegrity.generateMessageId(previousMessage);
|
||||||
// we start at a hash of the identity public key
|
|
||||||
message.id = await _generateInitialId(
|
|
||||||
identityPublicKey:
|
|
||||||
_activeAccountInfo.localAccount.identityMaster.identityPublicKey);
|
|
||||||
} else {
|
|
||||||
// If there is a last message, we generate the hash
|
|
||||||
// of the last message's signature and use it as our next id
|
|
||||||
message.id = await _hashSignature(previousMessage.signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now sign it
|
// Now sign it
|
||||||
await _signMessage(message);
|
await _senderMessageIntegrity.signMessage(
|
||||||
|
message, _activeAccountInfo.userLogin.identitySecret.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async process to send messages in the background
|
// Async process to send messages in the background
|
||||||
@ -303,17 +267,17 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
|
|
||||||
// Generate state for each message
|
// Generate state for each message
|
||||||
final sentMessagesMap =
|
final sentMessagesMap =
|
||||||
IMap<String, DHTLogElementState<proto.Message>>.fromValues(
|
IMap<String, OnlineElementState<proto.Message>>.fromValues(
|
||||||
keyMapper: (x) => x.value.uniqueIdString,
|
keyMapper: (x) => x.value.authorUniqueIdString,
|
||||||
values: sentMessages.elements,
|
values: sentMessages.elements,
|
||||||
);
|
);
|
||||||
final reconciledMessagesMap =
|
final reconciledMessagesMap =
|
||||||
IMap<String, proto.ReconciledMessage>.fromValues(
|
IMap<String, proto.ReconciledMessage>.fromValues(
|
||||||
keyMapper: (x) => x.content.uniqueIdString,
|
keyMapper: (x) => x.content.authorUniqueIdString,
|
||||||
values: reconciledMessages.elements,
|
values: reconciledMessages.elements,
|
||||||
);
|
);
|
||||||
final sendingMessagesMap = IMap<String, proto.Message>.fromValues(
|
final sendingMessagesMap = IMap<String, proto.Message>.fromValues(
|
||||||
keyMapper: (x) => x.uniqueIdString,
|
keyMapper: (x) => x.authorUniqueIdString,
|
||||||
values: sendingMessages,
|
values: sendingMessages,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -405,9 +369,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||||||
final TypedKey _remoteConversationRecordKey;
|
final TypedKey _remoteConversationRecordKey;
|
||||||
final TypedKey _remoteMessagesRecordKey;
|
final TypedKey _remoteMessagesRecordKey;
|
||||||
|
|
||||||
late final VeilidCrypto _messagesCrypto;
|
late final VeilidCrypto _conversationCrypto;
|
||||||
late final VeilidCryptoSystem _localMessagesCryptoSystem;
|
late final MessageIntegrity _senderMessageIntegrity;
|
||||||
late final VeilidCryptoSystem _identityCryptoSystem;
|
|
||||||
|
|
||||||
DHTLogCubit<proto.Message>? _sentMessagesCubit;
|
DHTLogCubit<proto.Message>? _sentMessagesCubit;
|
||||||
DHTLogCubit<proto.Message>? _rcvdMessagesCubit;
|
DHTLogCubit<proto.Message>? _rcvdMessagesCubit;
|
||||||
|
@ -127,7 +127,7 @@ class ChatComponent extends StatelessWidget {
|
|||||||
author: isLocal ? _localUser : _remoteUser,
|
author: isLocal ? _localUser : _remoteUser,
|
||||||
createdAt:
|
createdAt:
|
||||||
(message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(),
|
(message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(),
|
||||||
id: message.content.uniqueIdString,
|
id: message.content.authorUniqueIdString,
|
||||||
text: contextText.text,
|
text: contextText.text,
|
||||||
showStatus: status != null,
|
showStatus: status != null,
|
||||||
status: status);
|
status: status);
|
||||||
|
@ -16,13 +16,15 @@ Map<String, dynamic> reconciledMessageToJson(proto.ReconciledMessage m) =>
|
|||||||
m.writeToJsonMap();
|
m.writeToJsonMap();
|
||||||
|
|
||||||
extension MessageExt on proto.Message {
|
extension MessageExt on proto.Message {
|
||||||
Uint8List get uniqueIdBytes {
|
Uint8List get idBytes => Uint8List.fromList(id);
|
||||||
|
|
||||||
|
Uint8List get authorUniqueIdBytes {
|
||||||
final author = this.author.toVeilid().decode();
|
final author = this.author.toVeilid().decode();
|
||||||
final id = this.id;
|
final id = this.id;
|
||||||
return Uint8List.fromList([...author, ...id]);
|
return Uint8List.fromList([...author, ...id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get uniqueIdString => base64UrlNoPadEncode(uniqueIdBytes);
|
String get authorUniqueIdString => base64UrlNoPadEncode(authorUniqueIdBytes);
|
||||||
|
|
||||||
static int compareTimestamp(proto.Message a, proto.Message b) =>
|
static int compareTimestamp(proto.Message a, proto.Message b) =>
|
||||||
a.timestamp.compareTo(b.timestamp);
|
a.timestamp.compareTo(b.timestamp);
|
||||||
|
@ -9,16 +9,6 @@ import 'package:meta/meta.dart';
|
|||||||
|
|
||||||
import '../../../veilid_support.dart';
|
import '../../../veilid_support.dart';
|
||||||
|
|
||||||
@immutable
|
|
||||||
class DHTLogElementState<T> extends Equatable {
|
|
||||||
const DHTLogElementState({required this.value, required this.isOffline});
|
|
||||||
final T value;
|
|
||||||
final bool isOffline;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [value, isOffline];
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DHTLogStateData<T> extends Equatable {
|
class DHTLogStateData<T> extends Equatable {
|
||||||
const DHTLogStateData(
|
const DHTLogStateData(
|
||||||
@ -28,7 +18,7 @@ class DHTLogStateData<T> extends Equatable {
|
|||||||
required this.follow});
|
required this.follow});
|
||||||
// The view of the elements in the dhtlog
|
// The view of the elements in the dhtlog
|
||||||
// Span is from [tail-length, tail)
|
// Span is from [tail-length, tail)
|
||||||
final IList<DHTLogElementState<T>> elements;
|
final IList<OnlineElementState<T>> elements;
|
||||||
// One past the end of the last element
|
// One past the end of the last element
|
||||||
final int tail;
|
final int tail;
|
||||||
// The total number of elements to try to keep in 'elements'
|
// The total number of elements to try to keep in 'elements'
|
||||||
@ -92,7 +82,8 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
|
|||||||
|
|
||||||
Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit,
|
Future<void> _refreshInner(void Function(AsyncValue<DHTLogStateData<T>>) emit,
|
||||||
{bool forceRefresh = false}) async {
|
{bool forceRefresh = false}) async {
|
||||||
final avElements = await loadElements(_tail, _count);
|
final avElements = await operate(
|
||||||
|
(reader) => loadElementsFromReader(reader, _tail, _count));
|
||||||
final err = avElements.asError;
|
final err = avElements.asError;
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
emit(AsyncValue.error(err.error, err.stackTrace));
|
emit(AsyncValue.error(err.error, err.stackTrace));
|
||||||
@ -109,26 +100,22 @@ class DHTLogCubit<T> extends Cubit<DHTLogBusyState<T>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tail is one past the last element to load
|
// Tail is one past the last element to load
|
||||||
Future<AsyncValue<IList<DHTLogElementState<T>>>> loadElements(
|
Future<AsyncValue<IList<OnlineElementState<T>>>> loadElementsFromReader(
|
||||||
int tail, int count,
|
DHTLogReadOperations reader, int tail, int count,
|
||||||
{bool forceRefresh = false}) async {
|
{bool forceRefresh = false}) async {
|
||||||
await _initWait();
|
|
||||||
try {
|
try {
|
||||||
final allItems = await _log.operate((reader) async {
|
final length = reader.length;
|
||||||
final length = reader.length;
|
final end = ((tail - 1) % length) + 1;
|
||||||
final end = ((tail - 1) % length) + 1;
|
final start = (count < end) ? end - count : 0;
|
||||||
final start = (count < end) ? end - count : 0;
|
|
||||||
|
|
||||||
final offlinePositions = await reader.getOfflinePositions();
|
final offlinePositions = await reader.getOfflinePositions();
|
||||||
final allItems = (await reader.getRange(start,
|
final allItems = (await reader.getRange(start,
|
||||||
length: end - start, forceRefresh: forceRefresh))
|
length: end - start, forceRefresh: forceRefresh))
|
||||||
?.indexed
|
?.indexed
|
||||||
.map((x) => DHTLogElementState(
|
.map((x) => OnlineElementState(
|
||||||
value: _decodeElement(x.$2),
|
value: _decodeElement(x.$2),
|
||||||
isOffline: offlinePositions.contains(x.$1)))
|
isOffline: offlinePositions.contains(x.$1)))
|
||||||
.toIList();
|
.toIList();
|
||||||
return allItems;
|
|
||||||
});
|
|
||||||
if (allItems == null) {
|
if (allItems == null) {
|
||||||
return const AsyncValue.loading();
|
return const AsyncValue.loading();
|
||||||
}
|
}
|
||||||
|
12
packages/veilid_support/lib/src/online_element_state.dart
Normal file
12
packages/veilid_support/lib/src/online_element_state.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class OnlineElementState<T> extends Equatable {
|
||||||
|
const OnlineElementState({required this.value, required this.isOffline});
|
||||||
|
final T value;
|
||||||
|
final bool isOffline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [value, isOffline];
|
||||||
|
}
|
@ -10,6 +10,7 @@ export 'src/config.dart';
|
|||||||
export 'src/identity.dart';
|
export 'src/identity.dart';
|
||||||
export 'src/json_tools.dart';
|
export 'src/json_tools.dart';
|
||||||
export 'src/memory_tools.dart';
|
export 'src/memory_tools.dart';
|
||||||
|
export 'src/online_element_state.dart';
|
||||||
export 'src/output.dart';
|
export 'src/output.dart';
|
||||||
export 'src/persistent_queue.dart';
|
export 'src/persistent_queue.dart';
|
||||||
export 'src/protobuf_tools.dart';
|
export 'src/protobuf_tools.dart';
|
||||||
|
Loading…
Reference in New Issue
Block a user