diff --git a/circuitmatter/__init__.py b/circuitmatter/__init__.py index 86a13a8..f03bf1d 100644 --- a/circuitmatter/__init__.py +++ b/circuitmatter/__init__.py @@ -6,12 +6,12 @@ import time from . import case -from .clusters import core from . import data_model from . import interaction_model from .message import Message from .protocol import InteractionModelOpcode, ProtocolId, SecureProtocolOpcode from . import session +from .device_types.utility.root_node import RootNode __version__ = "0.0.0" @@ -56,38 +56,17 @@ def __init__( self._endpoints = {} self._next_endpoint = 0 - self._descriptor = data_model.DescriptorCluster() - self._descriptor.PartsList = [] - self._descriptor.ServerList = [] - self.add_cluster(0, self._descriptor) - basic_info = data_model.BasicInformationCluster() - basic_info.vendor_id = vendor_id - basic_info.product_id = product_id - basic_info.product_name = "CircuitMatter" - self.add_cluster(0, basic_info) - access_control = data_model.AccessControlCluster() - self.add_cluster(0, access_control) - group_keys = core.GroupKeyManagementCluster() - self.add_cluster(0, group_keys) - network_info = data_model.NetworkCommissioningCluster() - - ethernet = data_model.NetworkCommissioningCluster.NetworkInfoStruct() - ethernet.NetworkID = "enp13s0".encode("utf-8") - ethernet.Connected = True - network_info.networks = [ethernet] - network_info.connect_max_time_seconds = 10 - self.add_cluster(0, network_info) - general_commissioning = core.GeneralCommissioningCluster() - self.add_cluster(0, general_commissioning) - noc = core.NodeOperationalCredentialsCluster( - group_keys, random_source, self.mdns_server, self.UDP_PORT + self.root_node = RootNode( + random_source, self.mdns_server, self.UDP_PORT, vendor_id, product_id ) - self.add_cluster(0, noc) + self.add_device(self.root_node) self.vendor_id = vendor_id self.product_id = product_id - self.manager = session.SessionManager(self.random, self.socket, noc) + self.manager = session.SessionManager( + self.random, self.socket, self.root_node.noc + ) print(f"Listening on UDP port {self.UDP_PORT}") @@ -127,16 +106,28 @@ def add_cluster(self, endpoint, cluster): if endpoint not in self._endpoints: self._endpoints[endpoint] = {} if endpoint > 0: - self._descriptor.PartsList.append(endpoint) + self.root_node.descriptor.PartsList.append(endpoint) self._next_endpoint = max(self._next_endpoint, endpoint + 1) - if endpoint == 0: - self._descriptor.ServerList.append(cluster.CLUSTER_ID) self._endpoints[endpoint][cluster.CLUSTER_ID] = cluster def add_device(self, device): self._endpoints[self._next_endpoint] = {} if self._next_endpoint > 0: - self._descriptor.PartsList.append(self._next_endpoint) + self.root_node.descriptor.PartsList.append(self._next_endpoint) + + device.descriptor = data_model.DescriptorCluster() + device_type = data_model.DescriptorCluster.DeviceTypeStruct() + device_type.DeviceType = device.DEVICE_TYPE_ID + device_type.Revision = device.REVISION + device.descriptor.DeviceTypeList = [device_type] + device.descriptor.PartsList = [self._next_endpoint] + device.descriptor.ServerList = [] + device.descriptor.ClientList = [] + + for server in device.servers: + device.descriptor.ServerList.append(server.CLUSTER_ID) + self.add_cluster(self._next_endpoint, server) + self.add_cluster(self._next_endpoint, device.descriptor) self._next_endpoint += 1 def process_packets(self): @@ -203,6 +194,32 @@ def invoke(self, session, cluster, path, fields, command_ref): return response + def read_attribute_path(self, path): + attribute_reports = [] + if path.Endpoint is None: + endpoints = self._endpoints + else: + endpoints = [path.Endpoint] + + # Wildcard so we get it from every endpoint. + for endpoint in endpoints: + if path.Cluster is None: + clusters = self._endpoints[endpoint].values() + else: + if path.Cluster not in self._endpoints[endpoint]: + print( + f"Cluster 0x{path.Cluster:02x} not found on endpoint {endpoint}" + ) + continue + clusters = [self._endpoints[endpoint][path.Cluster]] + for cluster in clusters: + # TODO: The path object probably needs to be cloned. Otherwise we'll + # change the endpoint for all uses. + path.Endpoint = endpoint + path.Cluster = cluster.CLUSTER_ID + attribute_reports.extend(self.get_report(cluster, path)) + return attribute_reports + def process_packet(self, address, data): # Print the received data and the address of the sender # This is section 4.7.2 @@ -268,11 +285,8 @@ def process_packet(self, address, data): encoded = response.encode() exchange.commissioning_hash.update(encoded) - exchange.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.PBKDF_PARAM_RESPONSE, - response, - ) + print(response) + exchange.send(response) elif protocol_opcode == SecureProtocolOpcode.PBKDF_PARAM_RESPONSE: print("Received PBKDF Parameter Response") @@ -293,9 +307,7 @@ def process_packet(self, address, data): ) exchange.cA = cA exchange.Ke = Ke - exchange.send( - ProtocolId.SECURE_CHANNEL, SecureProtocolOpcode.PASE_PAKE2, pake2 - ) + exchange.send(pake2) elif protocol_opcode == SecureProtocolOpcode.PASE_PAKE2: print("Received PASE PAKE2") raise NotImplementedError("Implement SPAKE2+ prover") @@ -316,11 +328,7 @@ def process_packet(self, address, data): error_status.protocol_code = ( session.SecureChannelProtocolCode.INVALID_PARAMETER ) - exchange.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.STATUS_REPORT, - error_status, - ) + exchange.send(error_status) else: exchange.session.session_timestamp = time.monotonic() status_ok = session.StatusReport() @@ -329,11 +337,7 @@ def process_packet(self, address, data): status_ok.protocol_code = ( session.SecureChannelProtocolCode.SESSION_ESTABLISHMENT_SUCCESS ) - exchange.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.STATUS_REPORT, - status_ok, - ) + exchange.send(status_ok) # Fully initialize the secure session context we'll use going # forwards. @@ -349,16 +353,7 @@ def process_packet(self, address, data): ) response = self.manager.reply_to_sigma1(exchange, sigma1) - opcode = SecureProtocolOpcode.STATUS_REPORT - if isinstance(response, case.Sigma2Resume): - opcode = SecureProtocolOpcode.CASE_SIGMA2_RESUME - elif isinstance(response, case.Sigma2): - opcode = SecureProtocolOpcode.CASE_SIGMA2 - exchange.send( - ProtocolId.SECURE_CHANNEL, - opcode, - response, - ) + exchange.send(response) elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA2: print("Received CASE Sigma2") elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA3: @@ -378,17 +373,14 @@ def process_packet(self, address, data): error_status.general_code = general_code error_status.protocol_id = ProtocolId.SECURE_CHANNEL error_status.protocol_code = protocol_code - exchange.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.STATUS_REPORT, - error_status, - ) + exchange.send(error_status) elif protocol_opcode == SecureProtocolOpcode.CASE_SIGMA2_RESUME: print("Received CASE Sigma2 Resume") elif protocol_opcode == SecureProtocolOpcode.STATUS_REPORT: print("Received Status Report") report = session.StatusReport() report.decode(message.application_payload) + print(report) # Acknowledge the message because we have no further reply. if message.exchange_flags & session.ExchangeFlags.R: @@ -410,37 +402,25 @@ def process_packet(self, address, data): ) attribute_reports = [] for path in read_request.AttributeRequests: - if path.Endpoint is None: - # Wildcard so we get it from every endpoint. - for endpoint in self._endpoints: - if path.Cluster in self._endpoints[endpoint]: - cluster = self._endpoints[endpoint][path.Cluster] - # TODO: The path object probably needs to be cloned. Otherwise we'll - # change the endpoint for all uses. - path.Endpoint = endpoint - print(path.Endpoint) - print(path) - attribute_reports.extend(self.get_report(cluster, path)) - else: - print( - f"Cluster 0x{path.Cluster:02x} not found on endpoint {endpoint}" - ) - else: - if path.Cluster in self._endpoints[path.Endpoint]: - cluster = self._endpoints[path.Endpoint][path.Cluster] - attribute_reports.extend(self.get_report(cluster, path)) - else: - print(f"Cluster 0x{path.Cluster:02x} not found at all") - # attribute_reports.append( - # self._build_attribute_error(path, interaction_model.StatusCode.UNSUPPORTED_CLUSTER) - # ) + print("read", path) + attribute_reports.extend(self.read_attribute_path(path)) response = interaction_model.ReportDataMessage() response.AttributeReports = attribute_reports - exchange.send( - ProtocolId.INTERACTION_MODEL, - InteractionModelOpcode.REPORT_DATA, - response, + exchange.send(response) + elif protocol_opcode == InteractionModelOpcode.WRITE_REQUEST: + print("Received Write Request") + write_request, _ = interaction_model.WriteRequestMessage.decode( + message.application_payload[0], message.application_payload[1:] ) + print(write_request) + write_responses = [] + for request in write_request.WriteRequests: + path = request.Path + if path.Cluster in self._endpoints[path.Endpoint]: + cluster = self._endpoints[path.Endpoint][path.Cluster] + print(cluster) + write_responses.append(cluster.set_attribute(request)) + elif protocol_opcode == InteractionModelOpcode.INVOKE_REQUEST: print("Received Invoke Request") invoke_request, _ = interaction_model.InvokeRequestMessage.decode( @@ -482,11 +462,8 @@ def process_packet(self, address, data): response = interaction_model.InvokeResponseMessage() response.SuppressResponse = False response.InvokeResponses = invoke_responses - exchange.send( - ProtocolId.INTERACTION_MODEL, - InteractionModelOpcode.INVOKE_RESPONSE, - response, - ) + print("sending invoke response", response) + exchange.send(response) elif protocol_opcode == InteractionModelOpcode.INVOKE_RESPONSE: print("Received Invoke Response") elif protocol_opcode == InteractionModelOpcode.SUBSCRIBE_REQUEST: @@ -494,21 +471,24 @@ def process_packet(self, address, data): subscribe_request, _ = interaction_model.SubscribeRequestMessage.decode( message.application_payload[0], message.application_payload[1:] ) - error_status = session.StatusReport() - error_status.general_code = session.GeneralCode.UNSUPPORTED - error_status.protocol_id = ProtocolId.SECURE_CHANNEL - exchange.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.STATUS_REPORT, - error_status, - ) + print(subscribe_request) + attribute_reports = [] + for path in subscribe_request.AttributeRequests: + attribute_reports.extend(self.read_attribute_path(path)) + response = interaction_model.ReportDataMessage() + response.AttributeReports = attribute_reports + exchange.send(response) + final_response = interaction_model.SubscribeResponseMessage() + final_response.SubscriptionId = exchange.exchange_id + final_response.MaxInterval = subscribe_request.MaxIntervalCeiling + exchange.queue(final_response) elif protocol_opcode == InteractionModelOpcode.STATUS_RESPONSE: - print("Received Status Response") - print(message) status_response, _ = interaction_model.StatusResponseMessage.decode( message.application_payload[0], message.application_payload[1:] ) - print(status_response) + print( + f"Received Status Response on {message.session_id}/{message.exchange_id} ack {message.acknowledged_message_counter}: {status_response.Status!r}" + ) else: print(message) print("application payload", message.application_payload.hex(" ")) diff --git a/circuitmatter/__main__.py b/circuitmatter/__main__.py index d852064..f246e11 100644 --- a/circuitmatter/__main__.py +++ b/circuitmatter/__main__.py @@ -10,7 +10,7 @@ import circuitmatter as cm -from circuitmatter.device_types.lighting import extended_color +from circuitmatter.device_types.lighting import on_off class ReplaySocket: @@ -221,7 +221,7 @@ def socket(self, *args, **kwargs): return RecordingSocket(self.record_file, socket.socket(*args, **kwargs)) -class NeoPixel(extended_color.ExtendedColorLight): +class NeoPixel(on_off.OnOffLight): pass diff --git a/circuitmatter/case.py b/circuitmatter/case.py index cfab352..5f21566 100644 --- a/circuitmatter/case.py +++ b/circuitmatter/case.py @@ -1,9 +1,16 @@ from . import crypto +from . import protocol from . import session from . import tlv -class Sigma1(tlv.Structure): +class CASEMessage(tlv.Structure): + PROTOCOL_ID = protocol.ProtocolId.SECURE_CHANNEL + + +class Sigma1(CASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.CASE_SIGMA1 + initiatorRandom = tlv.OctetStringMember(1, 32) initiatorSessionId = tlv.IntMember(2, signed=False, octets=2) destinationId = tlv.OctetStringMember(3, crypto.HASH_LEN_BYTES) @@ -31,7 +38,8 @@ class Sigma2TbeData(tlv.Structure): resumptionID = tlv.OctetStringMember(4, 16) -class Sigma2(tlv.Structure): +class Sigma2(CASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.CASE_SIGMA2 responderRandom = tlv.OctetStringMember(1, 32) responderSessionId = tlv.IntMember(2, signed=False, octets=2) responderEphPubKey = tlv.OctetStringMember(3, crypto.PUBLIC_KEY_SIZE_BYTES) @@ -54,11 +62,13 @@ class Sigma3TbeData(tlv.Structure): signature = tlv.OctetStringMember(3, crypto.GROUP_SIZE_BYTES * 2) -class Sigma3(tlv.Structure): +class Sigma3(CASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.CASE_SIGMA3 encrypted3 = tlv.OctetStringMember(1, Sigma3TbeData.max_length()) -class Sigma2Resume(tlv.Structure): +class Sigma2Resume(CASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.CASE_SIGMA2_RESUME resumptionID = tlv.OctetStringMember(1, 16) sigma2ResumeMIC = tlv.OctetStringMember(2, 16) responderSessionID = tlv.IntMember(3, signed=False, octets=2) diff --git a/circuitmatter/clusters/core.py b/circuitmatter/clusters/core.py index d5d2e5c..1573311 100644 --- a/circuitmatter/clusters/core.py +++ b/circuitmatter/clusters/core.py @@ -101,8 +101,9 @@ def __init__(self, group_key_manager, random_source, mdns_server, port): self.nocs = [] self.fabrics = [] - self.commissioned_fabrics = 0 self.supported_fabrics = 10 + self.commissioned_fabrics = 0 + self.trusted_root_certificates = [] self.root_certs = [] self.compressed_fabric_ids = [] @@ -305,6 +306,8 @@ def add_noc( self.noc_keys.append(self.pending_signing_key) + self.trusted_root_certificates.append(self.pending_root_cert) + self.root_certs.append(root_cert) fabric_id = struct.pack(">Q", noc.subject.matter_fabric_id) self.compressed_fabric_ids.append( @@ -327,6 +330,24 @@ def add_noc( response.StatusCode = data_model.NodeOperationalCertStatusEnum.OK return response + def remove_fabric( + self, + session, + args: data_model.NodeOperationalCredentialsCluster.RemoveFabric, + ) -> data_model.NodeOperationalCredentialsCluster.NOCResponse: + index = args.FabricIndex + self.commissioned_fabrics -= 1 + + self.noc_keys[index] = None + self.root_certs[index] = None + self.compressed_fabric_ids[index] = None + self.fabrics[index] = None + self.nocs[index] = None + + response = data_model.NodeOperationalCredentialsCluster.NOCResponse() + response.StatusCode = data_model.NodeOperationalCertStatusEnum.OK + return response + class GroupKeyManagementCluster(data_model.GroupKeyManagementCluster): def __init__(self): diff --git a/circuitmatter/clusters/general/__init__.py b/circuitmatter/clusters/general/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/circuitmatter/clusters/general/identify.py b/circuitmatter/clusters/general/identify.py new file mode 100644 index 0000000..2bc5bf9 --- /dev/null +++ b/circuitmatter/clusters/general/identify.py @@ -0,0 +1,5 @@ +from circuitmatter.data_model import Cluster + + +class Identify(Cluster): + CLUSTER_ID = 0x0003 diff --git a/circuitmatter/clusters/general/on_off.py b/circuitmatter/clusters/general/on_off.py new file mode 100644 index 0000000..ed032d9 --- /dev/null +++ b/circuitmatter/clusters/general/on_off.py @@ -0,0 +1,30 @@ +from circuitmatter import data_model +from circuitmatter import tlv + + +class StartUpOnOffEnum(data_model.Enum8): + OFF = 0 + ON = 1 + TOGGLE = 2 + + +class OnOff(data_model.Cluster): + CLUSTER_ID = 0x0006 + + OnOff = data_model.BoolAttribute(0x0000, default=False) + GlobalSceneControl = data_model.BoolAttribute(0x4000, default=True) + OnTime = data_model.NumberAttribute(0x4001, signed=False, bits=16, default=0) + OffWaitTime = data_model.NumberAttribute(0x4002, signed=False, bits=16, default=0) + StartUpOnOff = data_model.EnumAttribute(0x4003, StartUpOnOffEnum) + + off = data_model.Command(0x00, None) + on = data_model.Command(0x01, None) + toggle = data_model.Command(0x02, None) + + class OffWithEffect(tlv.Structure): + EffectIdentifier = tlv.EnumMember(0, 0) + EffectVariant = tlv.EnumMember(1, 0, default=0) + + off_with_effect = data_model.Command(0x40, OffWithEffect) + on_with_recall_global_scene = data_model.Command(0x41, None) + on_with_timed_off = data_model.Command(0x42, None) diff --git a/circuitmatter/data_model.py b/circuitmatter/data_model.py index c33ac33..48cbd76 100644 --- a/circuitmatter/data_model.py +++ b/circuitmatter/data_model.py @@ -54,9 +54,25 @@ class List(tlv.ArrayMember): class Attribute: - def __init__(self, _id, default=None): + def __init__( + self, + _id, + default=None, + optional=False, + feature=0, + C_changes_omitted=False, + F_fixed=False, + N_nonvolatile=False, + P_reportable=False, + Q_quieter_reporting=False, + S_scene=False, + X_nullable=False, + ): self.id = _id self.default = default + self.optional = optional + self.feature = feature + self.nullable = X_nullable def __get__(self, instance, cls): v = instance._attribute_values.get(self.id, None) @@ -66,22 +82,28 @@ def __get__(self, instance, cls): def __set__(self, instance, value): old_value = instance._attribute_values.get(self.id, None) + print("set old_value", old_value) if old_value == value: return instance._attribute_values[self.id] = value instance.data_version += 1 + print("set new version", instance.data_version) - def encode(self, value): + def encode(self, value) -> bytes: + if value is None and self.nullable: + return b"\x14" # No tag, NULL + return self._encode(value) + + def _encode(self, value): raise NotImplementedError() class NumberAttribute(Attribute): - def __init__(self, _id, *, signed, bits, default=None): + def __init__(self, _id, *, signed, bits, **kwargs): self.signed = signed self.bits = bits self.id = _id - self.default = default - super().__init__(_id, default=default) + super().__init__(_id, **kwargs) @staticmethod def encode_number(value, *, signed=True) -> bytes: @@ -107,30 +129,35 @@ def encode_number(value, *, signed=True) -> bytes: return struct.pack(format_string, type | length, value) - def encode(self, value) -> bytes: + def _encode(self, value) -> bytes: return NumberAttribute.encode_number(value, signed=self.signed) -class FeatureMap(NumberAttribute): - def __init__(self): - super().__init__(0xFFFC, signed=False, bits=32, default=0) - - class EnumAttribute(NumberAttribute): - def __init__(self, _id, enum_type, default=None): + def __init__(self, _id, enum_type, **kwargs): self.enum_type = enum_type bits = 8 if issubclass(enum_type, Enum8) else 16 - super().__init__(_id, signed=False, bits=bits, default=default) + super().__init__(_id, signed=False, bits=bits, **kwargs) class ListAttribute(Attribute): - def __init__(self, _id, element_type): + def __init__(self, _id, element_type, **kwargs): self.tlv_type = tlv.ArrayMember(None, element_type) - super().__init__(_id) - - def encode(self, value) -> bytes: + self._element_type = element_type + # Copy the default list so we don't accidentally share it with another + # cluster of the same type. + if "default" in kwargs and isinstance(kwargs["default"], list): + kwargs["default"] = list(kwargs["default"]) + super().__init__(_id, **kwargs) + + def _encode(self, value) -> bytes: return self.tlv_type.encode(value) + def element_from_value(self, value): + if issubclass(self._element_type, tlv.Container): + return self._element_type.from_value(value) + return value + class BoolAttribute(Attribute): def encode(self, value) -> bytes: @@ -150,21 +177,24 @@ def encode(self, value) -> memoryview: class OctetStringAttribute(Attribute): - def __init__(self, _id, min_length, max_length): + def __init__(self, _id, min_length, max_length, **kwargs): self.min_length = min_length self.max_length = max_length - super().__init__(_id) + self.member = tlv.OctetStringMember(None, max_length=max_length) + super().__init__(_id, **kwargs) + + def encode(self, value): + return self.member.encode(value) class UTF8StringAttribute(Attribute): - def __init__(self, _id, min_length=0, max_length=1200, default=None): + def __init__(self, _id, min_length=0, max_length=1200, **kwargs): self.min_length = min_length self.max_length = max_length self.member = tlv.UTF8StringMember(None, max_length=max_length) - super().__init__(_id, default=default) + super().__init__(_id, **kwargs) def encode(self, value): - print(repr(value)) return self.member.encode(value) @@ -173,7 +203,13 @@ class BitmapAttribute(Attribute): class Command: - def __init__(self, command_id, request_type, response_id, response_type): + def __init__( + self, + command_id, + request_type, + response_id=None, + response_type=interaction_model.StatusCode, + ): self.command_id = command_id self.request_type = request_type self.response_id = response_id @@ -181,13 +217,16 @@ def __init__(self, command_id, request_type, response_id, response_type): class Cluster: - feature_map = FeatureMap() + feature_map = NumberAttribute(0xFFFC, signed=False, bits=32, default=0) def __init__(self): self._attribute_values = {} # Use random since this isn't for security or replayability. self.data_version = random.randint(0, 0xFFFFFFFF) + def __contains__(self, descriptor_id): + return descriptor_id in self._attribute_values + @classmethod def _attributes(cls) -> Iterable[tuple[str, Attribute]]: for superclass in cls.__mro__: @@ -202,8 +241,19 @@ def get_attribute_data( for field_name, descriptor in self._attributes(): if path.Attribute is not None and descriptor.id != path.Attribute: continue + if descriptor.feature and not (self.feature_map & descriptor.feature): + continue value = getattr(self, field_name) - print("reading", self, field_name, "->", value) + print( + "reading", + f"EP{path.Endpoint}", + type(self).__name__, + field_name, + "->", + value, + ) + if value is None and descriptor.optional: + continue data = interaction_model.AttributeDataIB() data.DataVersion = 0 attribute_path = interaction_model.AttributePathIB() @@ -219,6 +269,43 @@ def get_attribute_data( print("not found", path.Attribute) return replies + def set_attribute(self, attribute_data) -> interaction_model.AttributeStatusIB: + status_code = interaction_model.StatusCode.SUCCESS + for field_name, descriptor in self._attributes(): + path = attribute_data.Path + if path.Attribute is not None and descriptor.id != path.Attribute: + continue + has_list_index = False + for entry in path: + if ( + isinstance(entry, tuple) + and entry[0] == interaction_model.AttributePathIB.ListIndex + ): + has_list_index = True + break + # value = + value = attribute_data.Data + print("writing", self, field_name, "->", value, "?", has_list_index) + + if has_list_index: + if not isinstance(descriptor, ListAttribute): + status_code = interaction_model.StatusCode.UNSUPPORTED_WRITE + break + list_ = getattr(self, field_name) + if not isinstance(list_, list): + status_code = interaction_model.StatusCode.UNSUPPORTED_WRITE + break + list_.append(descriptor.element_from_value(value)) + else: + setattr(self, field_name, value) + astatus = interaction_model.AttributeStatusIB() + astatus.Path = attribute_data.Path + status = interaction_model.StatusIB() + status.Status = status_code + status.ClusterStatus = 0 + astatus.Status = status + return astatus + @classmethod def _commands(cls) -> Iterable[tuple[str, Command]]: for superclass in cls.__mro__: @@ -238,14 +325,28 @@ def invoke( command = getattr(self, field_name) if callable(command): if descriptor.request_type is not None: - arg = descriptor.request_type.from_value(fields) - result = command(session, arg) + try: + arg = descriptor.request_type.from_value(fields) + except ValueError: + return interaction_model.StatusCode.INVALID_COMMAND + try: + result = command(session, arg) + except Exception as e: + print(e) + return interaction_model.StatusCode.FAILURE else: - result = command(session) + try: + result = command(session) + except Exception as e: + print(e) + return interaction_model.StatusCode.FAILURE else: - print(field_name, "not implemented") - return None - if descriptor.response_type is not None: + return interaction_model.StatusCode.UNSUPPORTED_COMMAND + if descriptor.response_type is interaction_model.StatusCode: + if result is None: + return interaction_model.StatusCode.SUCCESS + return result + elif descriptor.response_type is not None: cdata = interaction_model.CommandDataIB() response_path = interaction_model.CommandPathIB() response_path.Endpoint = path.Endpoint @@ -255,8 +356,7 @@ def invoke( if result: cdata.CommandFields = descriptor.response_type.encode(result) return cdata - else: - return result + return result if not found: print("not found", path.Command) return None @@ -266,8 +366,8 @@ class DescriptorCluster(Cluster): CLUSTER_ID = 0x001D class DeviceTypeStruct(tlv.Structure): - devtype_id = tlv.IntMember(0, signed=False, octets=4) - revision = tlv.IntMember(1, signed=False, octets=2, minimum=1) + DeviceType = DeviceTypeId(0) + Revision = Uint16(1, minimum=1) DeviceTypeList = ListAttribute(0x0000, DeviceTypeStruct) ServerList = ListAttribute(0x0001, ClusterId()) @@ -307,16 +407,16 @@ class AccessControlCluster(Cluster): CLUSTER_ID = 0x001F class AccessControlEntryStruct(tlv.Structure): - Privilege = tlv.EnumMember(0, AccessControlEntryPrivilegeEnum) - AuthMode = tlv.EnumMember(1, AccessControlEntryAuthModeEnum) - Subjects = List(2, Uint64()) - Targets = List(3, AccessControlTargetStruct) + Privilege = tlv.EnumMember(1, AccessControlEntryPrivilegeEnum) + AuthMode = tlv.EnumMember(2, AccessControlEntryAuthModeEnum) + Subjects = List(3, Uint64()) + Targets = List(4, AccessControlTargetStruct, nullable=True) class AccessControlExtensionStruct(tlv.Structure): Data = tlv.OctetStringMember(1, max_length=128) ACL = ListAttribute(0x0000, AccessControlEntryStruct) - Extension = ListAttribute(0x0001, AccessControlExtensionStruct) + Extension = ListAttribute(0x0001, AccessControlExtensionStruct, optional=True) SubjectsPerAccessControlEntry = NumberAttribute( 0x0002, signed=False, bits=16, default=4 ) @@ -450,8 +550,8 @@ class GroupInfoMapStruct(tlv.Structure): class KeySetWrite(tlv.Structure): GroupKeySet = tlv.StructMember(0, GroupKeySetStruct) - group_key_map = ListAttribute(0, GroupKeyMapStruct) - group_table = ListAttribute(1, GroupInfoMapStruct) + group_key_map = ListAttribute(0, GroupKeyMapStruct, default=[]) + group_table = ListAttribute(1, GroupInfoMapStruct, default=[]) max_groups_per_fabric = NumberAttribute(2, signed=False, bits=16, default=0) max_group_keys_per_fabric = NumberAttribute(3, signed=False, bits=16, default=1) @@ -579,17 +679,43 @@ class NetworkInfoStruct(tlv.Structure): NetworkID = tlv.OctetStringMember(0, min_length=1, max_length=32) Connected = tlv.BoolMember(1) - max_networks = NumberAttribute(0, signed=False, bits=8) + max_networks = NumberAttribute(0, signed=False, bits=8, default=1, F_fixed=True) networks = ListAttribute(1, NetworkInfoStruct) - scan_max_time_seconds = NumberAttribute(2, signed=False, bits=8) - connect_max_time_seconds = NumberAttribute(3, signed=False, bits=8) - interface_enabled = BoolAttribute(4) - last_network_status = EnumAttribute(5, NetworkCommissioningStatus) - last_network_id = OctetStringAttribute(6, min_length=1, max_length=32) - last_connect_error_value = NumberAttribute(7, signed=True, bits=32) - supported_wifi_bands = ListAttribute(8, WifiBandEnum) - supported_thread_features = BitmapAttribute(9) - thread_version = NumberAttribute(10, signed=False, bits=16) + scan_max_time_seconds = NumberAttribute( + 2, + signed=False, + bits=8, + feature=FeatureBitmap.WIFI_NETWORK_INTERFACE + | FeatureBitmap.THREAD_NETWORK_INTERFACE, + F_fixed=True, + ) + connect_max_time_seconds = NumberAttribute( + 3, + signed=False, + bits=8, + feature=FeatureBitmap.WIFI_NETWORK_INTERFACE + | FeatureBitmap.THREAD_NETWORK_INTERFACE, + F_fixed=True, + ) + interface_enabled = BoolAttribute(4, default=True, N_nonvolatile=True) + last_network_status = EnumAttribute(5, NetworkCommissioningStatus, X_nullable=True) + last_network_id = OctetStringAttribute( + 6, min_length=1, max_length=32, X_nullable=True + ) + last_connect_error_value = NumberAttribute(7, signed=True, bits=32, X_nullable=True) + supported_wifi_bands = ListAttribute( + 8, WifiBandEnum, feature=FeatureBitmap.WIFI_NETWORK_INTERFACE, F_fixed=True + ) + supported_thread_features = BitmapAttribute( + 9, feature=FeatureBitmap.THREAD_NETWORK_INTERFACE, F_fixed=True + ) + thread_version = NumberAttribute( + 10, + signed=False, + bits=16, + feature=FeatureBitmap.THREAD_NETWORK_INTERFACE, + F_fixed=True, + ) class CertificateChainTypeEnum(Enum8): @@ -683,11 +809,13 @@ class RemoveFabric(tlv.Structure): class AddTrustedRootCertificate(tlv.Structure): RootCACertificate = tlv.OctetStringMember(0, 400) - nocs = ListAttribute(0, NOCStruct) - fabrics = ListAttribute(1, FabricDescriptorStruct) - supported_fabrics = NumberAttribute(2, signed=False, bits=8) - commissioned_fabrics = NumberAttribute(3, signed=False, bits=8) - trusted_root_certificates = ListAttribute(4, tlv.OctetStringMember(None, 400)) + nocs = ListAttribute(0, NOCStruct, N_nonvolatile=True, C_changes_omitted=True) + fabrics = ListAttribute(1, FabricDescriptorStruct, N_nonvolatile=True) + supported_fabrics = NumberAttribute(2, signed=False, bits=8, F_fixed=True) + commissioned_fabrics = NumberAttribute(3, signed=False, bits=8, N_nonvolatile=True) + trusted_root_certificates = ListAttribute( + 4, tlv.OctetStringMember(None, 400), N_nonvolatile=True, C_changes_omitted=True + ) current_fabric_index = NumberAttribute(5, signed=False, bits=8, default=0) attestation_request = Command(0x00, AttestationRequest, 0x01, AttestationResponse) diff --git a/circuitmatter/device_types/lighting/on_off.py b/circuitmatter/device_types/lighting/on_off.py index b22e96e..8e6e381 100644 --- a/circuitmatter/device_types/lighting/on_off.py +++ b/circuitmatter/device_types/lighting/on_off.py @@ -1,2 +1,26 @@ +from circuitmatter.clusters.general.identify import Identify +from circuitmatter.clusters.general.on_off import OnOff + + class OnOffLight: DEVICE_TYPE_ID = 0x0100 + REVISION = 3 + + def __init__(self): + self.servers = [] + + self._identify = Identify() + self.servers.append(self._identify) + + self._on_off = OnOff() + self._on_off.on = self.on + self._on_off.off = self.off + self.servers.append(self._on_off) + + def on(self, session): + print("on!") + self._on_off.on_off = True + + def off(self, session): + print("off!") + self._on_off.on_off = False diff --git a/circuitmatter/device_types/utility/__init__.py b/circuitmatter/device_types/utility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/circuitmatter/device_types/utility/root_node.py b/circuitmatter/device_types/utility/root_node.py new file mode 100644 index 0000000..d573b3d --- /dev/null +++ b/circuitmatter/device_types/utility/root_node.py @@ -0,0 +1,41 @@ +from circuitmatter import data_model +from circuitmatter.clusters import core + + +class RootNode: + DEVICE_TYPE_ID = 0x0011 + REVISION = 2 + + def __init__(self, random_source, mdns_server, port, vendor_id, product_id): + self.servers = [] + + basic_info = data_model.BasicInformationCluster() + basic_info.vendor_id = vendor_id + basic_info.product_id = product_id + basic_info.product_name = "CircuitMatter" + self.servers.append(basic_info) + access_control = data_model.AccessControlCluster() + self.servers.append(access_control) + group_keys = core.GroupKeyManagementCluster() + self.servers.append(group_keys) + network_info = data_model.NetworkCommissioningCluster() + network_info.feature_map = ( + data_model.NetworkCommissioningCluster.FeatureBitmap.WIFI_NETWORK_INTERFACE + ) + + ethernet = data_model.NetworkCommissioningCluster.NetworkInfoStruct() + ethernet.NetworkID = "enp13s0".encode("utf-8") + ethernet.Connected = True + network_info.networks = [ethernet] + network_info.connect_max_time_seconds = 10 + network_info.last_network_status = ( + data_model.NetworkCommissioningCluster.NetworkCommissioningStatus.SUCCESS + ) + network_info.last_network_id = ethernet.NetworkID + self.servers.append(network_info) + general_commissioning = core.GeneralCommissioningCluster() + self.servers.append(general_commissioning) + self.noc = core.NodeOperationalCredentialsCluster( + group_keys, random_source, mdns_server, port + ) + self.servers.append(self.noc) diff --git a/circuitmatter/exchange.py b/circuitmatter/exchange.py index c31e569..247451b 100644 --- a/circuitmatter/exchange.py +++ b/circuitmatter/exchange.py @@ -2,6 +2,7 @@ from .message import Message, ExchangeFlags, ProtocolId from .protocol import SecureProtocolOpcode +from .interaction_model import ChunkedMessage # Section 4.12.8 MRP_MAX_TRANSMISSIONS = 5 @@ -38,10 +39,17 @@ def __init__(self, session, initiator: bool, exchange_id: int, protocols): """When to next resend the message that hasn't been acked""" self.pending_retransmission = None """Message that we've attempted to send but hasn't been acked""" + self.pending_payloads = [] def send( - self, protocol_id, protocol_opcode, application_payload=None, reliable=True + self, + application_payload=None, + protocol_id=None, + protocol_opcode=None, + reliable=True, ): + if self.pending_retransmission is not None: + raise RuntimeError("Cannot send a message while waiting for an ack.") message = Message() message.exchange_flags = ExchangeFlags(0) if self.initiator: @@ -55,26 +63,44 @@ def send( message.exchange_flags |= ExchangeFlags.R self.pending_retransmission = message message.source_node_id = self.session.local_node_id + if protocol_id is None: + protocol_id = application_payload.PROTOCOL_ID message.protocol_id = protocol_id + if protocol_opcode is None: + protocol_opcode = application_payload.PROTOCOL_OPCODE message.protocol_opcode = protocol_opcode message.exchange_id = self.exchange_id - message.application_payload = application_payload + if isinstance(application_payload, ChunkedMessage): + chunk = memoryview(bytearray(1280))[:1200] + offset = application_payload.encode_into(chunk) + print(chunk[:offset].hex()) + if application_payload.MoreChunkedMessages: + self.pending_payloads.insert(0, application_payload) + message.application_payload = chunk[:offset] + else: + message.application_payload = application_payload self.session.send(message) def send_standalone(self): if self.pending_retransmission is not None: self.session.send(self.pending_retransmission) return + if self.pending_payloads: + self.send(self.pending_payloads.pop(0)) + return self.send( - ProtocolId.SECURE_CHANNEL, - SecureProtocolOpcode.MRP_STANDALONE_ACK, - None, + protocol_id=ProtocolId.SECURE_CHANNEL, + protocol_opcode=SecureProtocolOpcode.MRP_STANDALONE_ACK, reliable=False, ) + def queue(self, payload): + self.pending_payloads.append(payload) + def receive(self, message) -> bool: """Process the message and return if the packet should be dropped.""" # Section 4.12.5.2.1 + print(message) if message.exchange_flags & ExchangeFlags.A: if message.acknowledged_message_counter is None: # Drop messages that are missing an acknowledgement counter. @@ -86,6 +112,7 @@ def receive(self, message) -> bool: ): # Drop messages that have the wrong acknowledgement counter. return True + print("acknowledged", message.acknowledged_message_counter) self.pending_retransmission = None self.next_retransmission_time = None diff --git a/circuitmatter/interaction_model.py b/circuitmatter/interaction_model.py index 6929c9c..e93db87 100644 --- a/circuitmatter/interaction_model.py +++ b/circuitmatter/interaction_model.py @@ -1,5 +1,6 @@ import enum +from . import protocol from . import tlv @@ -126,10 +127,37 @@ class AttributeReportIB(tlv.Structure): class InteractionModelMessage(tlv.Structure): + PROTOCOL_ID = protocol.ProtocolId.INTERACTION_MODEL + InteractionModelRevision = tlv.IntMember(0xFF, signed=False, octets=1, default=11) +class ChunkedMessage(InteractionModelMessage): + """Chunked messages take multiple encodes or decodes before they are complete.""" + + def encode_into(self, buffer: memoryview, offset: int = 0) -> int: + # Leave room for MoreChunkedMessages, SupressResponse, and InteractionModelRevision. + buffer[0] = tlv.ElementType.STRUCTURE + offset += 1 + subbuffer = buffer[: -2 * 2 - 3 - 1] + del self.MoreChunkedMessages + for name, descriptor_class in self._members(): + try: + print("encoding", name, "at offset", offset) + offset = descriptor_class.encode_into(self, subbuffer, offset) + except tlv.ArrayEncodingError as e: + print("splitting", name, f"[{e.index}:] offset {offset}") + offset = e.offset + tag = descriptor_class.tag + self.values[tag] = self.values[tag][e.index :] + self.MoreChunkedMessages = True + buffer[offset] = tlv.ElementType.END_OF_CONTAINER + return offset + 1 + + class ReadRequestMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.READ_REQUEST + AttributeRequests = tlv.ArrayMember(0, AttributePathIB) EventRequests = tlv.ArrayMember(1, EventPathIB) EventFilters = tlv.ArrayMember(2, EventFilterIB) @@ -137,6 +165,21 @@ class ReadRequestMessage(InteractionModelMessage): DataVersionFilters = tlv.ArrayMember(4, DataVersionFilterIB) +class WriteRequestMessage(ChunkedMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.WRITE_REQUEST + + SuppressResponse = tlv.BoolMember(0, optional=True) + TimedRequest = tlv.BoolMember(1) + WriteRequests = tlv.ArrayMember(2, AttributeDataIB) + MoreChunkedMessages = tlv.BoolMember(3, optional=True) + + +class WriteResponseMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.WRITE_RESPONSE + + WriteResponses = tlv.ArrayMember(0, AttributeStatusIB) + + class EventStatusIB(tlv.Structure): Path = tlv.StructMember(0, EventPathIB) Status = tlv.StructMember(1, StatusIB) @@ -161,7 +204,9 @@ class EventReportIB(tlv.Structure): EventData = tlv.StructMember(1, EventDataIB) -class ReportDataMessage(InteractionModelMessage): +class ReportDataMessage(ChunkedMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.REPORT_DATA + SubscriptionId = tlv.IntMember(0, signed=False, octets=4, optional=True) AttributeReports = tlv.ArrayMember(1, AttributeReportIB, optional=True) EventReports = tlv.ArrayMember(2, EventReportIB, optional=True) @@ -193,18 +238,24 @@ class InvokeResponseIB(tlv.Structure): class InvokeRequestMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.INVOKE_REQUEST + SuppressResponse = tlv.BoolMember(0) TimedRequest = tlv.BoolMember(1) InvokeRequests = tlv.ArrayMember(2, CommandDataIB) class InvokeResponseMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.INVOKE_RESPONSE + SuppressResponse = tlv.BoolMember(0) InvokeResponses = tlv.ArrayMember(1, InvokeResponseIB) MoreChunkedMessages = tlv.BoolMember(2, optional=True) class SubscribeRequestMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.SUBSCRIBE_REQUEST + KeepSubscriptions = tlv.BoolMember(0) MinIntervalFloor = tlv.IntMember(1, signed=False, octets=2) MaxIntervalCeiling = tlv.IntMember(2, signed=False, octets=2) @@ -216,4 +267,13 @@ class SubscribeRequestMessage(InteractionModelMessage): class StatusResponseMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.STATUS_RESPONSE + Status = tlv.EnumMember(0, StatusCode) + + +class SubscribeResponseMessage(InteractionModelMessage): + PROTOCOL_OPCODE = protocol.InteractionModelOpcode.SUBSCRIBE_RESPONSE + + SubscriptionId = tlv.IntMember(0, signed=False, octets=4) + MaxInterval = tlv.IntMember(2, signed=False, octets=2) diff --git a/circuitmatter/pase.py b/circuitmatter/pase.py index ea182bf..ff56a0f 100644 --- a/circuitmatter/pase.py +++ b/circuitmatter/pase.py @@ -1,4 +1,5 @@ from . import crypto +from . import protocol from . import tlv from . import session @@ -11,6 +12,10 @@ from ecdsa.curves import NIST256p +class PASEMessage(tlv.Structure): + PROTOCOL_ID = protocol.ProtocolId.SECURE_CHANNEL + + # pbkdfparamreq-struct => STRUCTURE [ tag-order ] # { # initiatorRandom @@ -23,7 +28,9 @@ # [4] : BOOLEAN, # initiatorSessionParams [5, optional] : session-parameter-struct # } -class PBKDFParamRequest(tlv.Structure): +class PBKDFParamRequest(PASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.PBKDF_PARAM_REQUEST + initiatorRandom = tlv.OctetStringMember(1, 32) initiatorSessionId = tlv.IntMember(2, signed=False, octets=2) passcodeId = tlv.IntMember(3, signed=False, octets=2) @@ -55,7 +62,8 @@ class Crypto_PBKDFParameterSet(tlv.Structure): # [4] : Crypto_PBKDFParameterSet, # responderSessionParams [5, optional] : session-parameter-struct # } -class PBKDFParamResponse(tlv.Structure): +class PBKDFParamResponse(PASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.PBKDF_PARAM_RESPONSE initiatorRandom = tlv.OctetStringMember(1, 32) responderRandom = tlv.OctetStringMember(2, 32) responderSessionId = tlv.IntMember(3, signed=False, octets=2) @@ -65,16 +73,19 @@ class PBKDFParamResponse(tlv.Structure): ) -class PAKE1(tlv.Structure): +class PAKE1(PASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.PASE_PAKE1 pA = tlv.OctetStringMember(1, crypto.PUBLIC_KEY_SIZE_BYTES) -class PAKE2(tlv.Structure): +class PAKE2(PASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.PASE_PAKE2 pB = tlv.OctetStringMember(1, crypto.PUBLIC_KEY_SIZE_BYTES) cB = tlv.OctetStringMember(2, crypto.HASH_LEN_BYTES) -class PAKE3(tlv.Structure): +class PAKE3(PASEMessage): + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.PASE_PAKE3 cA = tlv.OctetStringMember(1, crypto.HASH_LEN_BYTES) diff --git a/circuitmatter/session.py b/circuitmatter/session.py index 247f3fc..3cb3131 100644 --- a/circuitmatter/session.py +++ b/circuitmatter/session.py @@ -107,6 +107,9 @@ class SecureChannelProtocolCode(enum.IntEnum): class StatusReport: + PROTOCOL_ID = protocol.ProtocolId.SECURE_CHANNEL + PROTOCOL_OPCODE = protocol.SecureProtocolOpcode.STATUS_REPORT + def __init__(self): self.clear() diff --git a/circuitmatter/tlv.py b/circuitmatter/tlv.py index d726678..cff6f66 100644 --- a/circuitmatter/tlv.py +++ b/circuitmatter/tlv.py @@ -131,6 +131,10 @@ def _members_by_tag(cls) -> dict[int, tuple[str, Member]]: def set_value(self, tag, value): self.values[tag] = value + def delete_value(self, tag): + if tag in self.values: + del self.values[tag] + class Structure(Container): def __str__(self): @@ -251,6 +255,8 @@ def __get__( ) -> _T: ... def __get__(self, obj, objtype=None): + if obj is None: + return self.tag if self.tag in obj.values: return obj.values[self.tag] return self._default @@ -272,6 +278,11 @@ def __set__(self, obj, value): raise ValueError("Not nullable") obj.set_value(self.tag, value) + def __delete__(self, obj): + if not self.optional: + raise ValueError("Not optional") + obj.delete_value(self.tag) + def encode(self, value): buffer = memoryview(bytearray(self.max_length)) end = self._encode_value_into(value, buffer, 0, anonymous_ok=True) @@ -301,7 +312,9 @@ def _encode_value_into( # Value is None and the field is optional so skip it. return offset elif not self.nullable: - raise ValueError(f"{self._name} isn't set") + raise ValueError( + f"{self._name} ({type(self).__name__}) isn't set and not nullable or optional" + ) tag_control = 0 if self.tag is not None: @@ -380,7 +393,14 @@ def print(self, value: _T) -> str: "Return string representation of `value`" ... - def from_value(cls, value): + def from_value(self, value): + if value is None: + if not self.nullable: + raise ValueError("Member not nullable") + return None + return self._from_value(value) + + def _from_value(self, value): return value @@ -717,10 +737,16 @@ def encode_value_into(self, value, buffer: bytearray, offset: int) -> int: offset = value.encode_into(buffer, offset) return offset - def from_value(self, value): + def _from_value(self, value): return self.substruct_class.from_value(value) +class ArrayEncodingError(Exception): + def __init__(self, index, offset): + self.index = index + self.offset = offset + + class ArrayMember(Member[_TLVStruct, _OPT, _NULLABLE]): def __init__( self, @@ -758,22 +784,29 @@ def print(self, value): def encode_element_type(self, value): return ElementType.ARRAY - def encode_value_into(self, value, buffer: bytearray, offset: int) -> int: - for v in value: + def encode_value_into(self, value, buffer: memoryview, offset: int) -> int: + subbuffer = buffer[:-1] + for i, v in enumerate(value): if isinstance(v, Structure): buffer[offset] = ElementType.STRUCTURE elif isinstance(v, List): buffer[offset] = ElementType.LIST elif isinstance(self.substruct_class, Member): buffer[offset] = self.substruct_class.encode_element_type(v) - print(offset, hex(buffer[offset])) - offset = self.substruct_class.encode_value_into(v, buffer, offset + 1) + offset = self.substruct_class.encode_value_into( + v, subbuffer, offset + 1 + ) continue - offset = v.encode_into(buffer, offset + 1) + try: + offset = v.encode_into(buffer, offset + 1) + except (ValueError, IndexError): + # If we run out of room, mark our end and raise an exception. + buffer[offset] = ElementType.END_OF_CONTAINER + raise ArrayEncodingError(i - 1, offset + 1) buffer[offset] = ElementType.END_OF_CONTAINER return offset + 1 - def from_value(self, value): + def _from_value(self, value): for i in range(len(value)): value[i] = self.substruct_class.from_value(value[i]) return value @@ -857,8 +890,14 @@ def set_value(self, tag, value): i = self.items.index((tag, self.values[tag])) self.items[i] = (tag, value) else: - self.values[tag] = value self.items.append((tag, value)) + self.values[tag] = value + + def delete_value(self, tag): + for item in self.items: + if item[0] == tag: + self.items.remove(item) + del self.values[tag] _TLVList = TypeVar("_TLVList", bound=List) @@ -903,7 +942,7 @@ def encode_value_into(self, value, buffer: bytearray, offset: int) -> int: offset = value.encode_into(buffer, offset) return offset - def from_value(self, value): + def _from_value(self, value): return self.substruct_class.from_value(value) diff --git a/test_data/recorded_packets.jsonl b/test_data/recorded_packets.jsonl index bcd7ff7..2d55e73 100644 --- a/test_data/recorded_packets.jsonl +++ b/test_data/recorded_packets.jsonl @@ -1,66 +1,79 @@ -["urandom", 85046090416534, 4, "4IO8vw=="] -["urandom", 85046090434497, 4, "qy6AXw=="] -["urandom", 85046090444045, 4, "Nyig8g=="] -["urandom", 85046090453824, 4, "drAwgg=="] -["urandom", 85046097552057, 8, "x7lfor1ndC8="] -["receive", 85052824193293, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADBBag7kypolx2h2jgUg4ukAABUwASBL9wjf+vfsHVfhvMHn1i2t/tU9Ygtg850gOeMEFNpwvCUCDzwkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] -["urandom", 85052824490293, 32, "iH6SzwMwfZqZgRp9C74zX/MTD1RfzzRenrj4F8sZUTw="] -["urandom", 85052824508277, 4, "KoQtLQ=="] -["send", 85052824730065, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AQAAAEDI+wvkypolx2h2jgYh4ukAADBBag4VMAEgS/cI3/r37B1X4bzB59Ytrf7VPWILYPOdIDnjBBTacLwwAiCIfpLPAzB9mpmBGn0LvjNf8xMPVF/PNF6euPgXyxlRPCQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] -["receive", 85052973269607, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADFBag7kypolx2h2jgci4ukAAEDI+wsVMAFBBFk1C3wXWT68BqpoOHlZg1dzVYcIBb5WDlxKGp63sQa6trJ7Pdg5owflFUQNevZBYH18z7/HsL6nIEldBW96KKMY"] -["randbelow", 85052973433606, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 108631079982384723248405167285597273137560953659345445474004269921692677910752] -["send", 85052983277236, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AQAAAEHI+wvkypolx2h2jgYj4ukAADFBag4VMAFBBOdTpIWqGo46gRjbkRpBFTFRbIwCbt5ZQ2HlEol9VVwRnXwxrutMXajB1uOt+FQHuGfqp6O190ig4OHxI7D4IvMwAiAFA0GOYlifwN/Nmg0a9txeIqEj+HxDPMO4m00zXQQNTBg="] -["receive", 85053001749013, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADJBag7kypolx2h2jgck4ukAAEHI+wsVMAEgGdNOs6Iszjhw4bcdvQJprmP46QFlXugHVO8ltcBxKR0Y"] -["send", 85053001863118, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AQAAAELI+wvkypolx2h2jgZA4ukAADJBag4AAAAAAAAAAA=="] -["receive", 85053004350648, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADNBag7kypolx2h2jgMQ4ukAAELI+ws="] -["receive", 85053004744230, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAA6xGgW/bplcUR61VbDRCeruJfMEvcDebUARFfnBR9hp64UyMT13pehNKgHQ6seIl0/d5gVEe0T8biCWTlH/TK6gUIoVhXtlidDPCoy2nsn6ym/uhVno/ZN6SNkQNvYa1eymOsstw+IYUzK+NksXE1Y2E1txPAduqzBlcojJ9L8="] -["send", 85053005705172, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AETY0gIvZudOc4piWLUFCNRMQPZc4uKwsn/PQyGwtzZgu3lJwEzZxcdSDe/ze7Y8H+gOuhwSHIaszvmom4caZZqLficBdOrk/KfK8+a/G4Wwm2CeIYK9lzKzq50FKjcKMDhi+DjLM244JV76Lww6SjlgUwtn9X1Fz2aD29POyfRpVa3UCzfmuBBgFPAj0DX7gfCPg8n//gjIexXXBNB5Ycdj6QMpUxyIu/FVq+wWzgvd879Y+fjKJhhPrdwzhvknSPLyzItMyEQsruhWC/FrTTokadVXFYC/0c9hprK+lI5ZwuYEY/ea8/o="] -["receive", 85053009604805, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAA+xGgVysgoGaUbEobIk8tAS2rRDwR76Fi2/Xl5gzGdicdTYcEgR"] -["receive", 85053009748756, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABCxGgXcvgZUxJjyj4tR9dBG1PTXUJJoPVbscmSug/KSPdq+duFaqPxnyAhn+J2Ix9igld8kR4M3SbtH4+SwL3orchi1BA=="] -["send", 85053010015499, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEXY0gLt+G3sd42d6a2HYfAkmU4RTyn5CEICc4XrgG8tEcXS/iVU4f83tAYA3ARthO8aO+ui5vVBeePxgw=="] -["receive", 85053012830136, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABGxGgU0ED9vU10HQHxHKGxhAkA/b88f3Wl02Y52OF4XWnP9CQpv"] -["receive", 85053012964690, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABKxGgW+Z6Y0n4FJfuVAkifLsoe3u6ff8ICVdqtmy+hSOQuRbrJc0DAyK2I2Ms7mKeqpgDj+uhlkJ+buglM="] -["send", 85053013285354, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEbY0gK809zPVpenH98v87gf4MAEGRS/K0+EOaiHLNX4GcsxLdprTteXZcsqTRmQxcZkK6BAC882aZ4yxOmAgUjLqg=="] -["receive", 85053016324845, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABOxGgUnPCey+aJo72ETBYsmbclM/G5zeDiCBD0HINKAHc671vq4DkdzsPYcmvdfrL//8iNnES+wCYtmysqnbek53Q=="] -["send", 85053016622666, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEfY0gKZBKxnFrRGSdh6iOSJ4ZTWTlV5S9nzTHiyfklWwO4c8c78iGWjiYlnwLp5EJOCQsVc2cxUnSTh0aj7WvgBvQ=="] -["receive", 85053016651381, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABSxGgWmXWPg361fITe5i8Uk8pUotRIE7yPwcVfBwQ=="] -["receive", 85053419555740, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABOxGgUnPCey+aJo72ETBYsmbclM/G5zeDiCBD0HINKAHc671vq4DkdzsPYcmvdfrL//8iNnES+wCYtmysqnbek53Q=="] -["send", 85053419985140, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEjY0gJ63PajTASOzOfUGVizJ7bSRL49m/w7HCtK05EKLgNX2HBOZFDOjauJcp53S9836jc/BuHd7PbB29KpD4ZhBw=="] -["receive", 85053425530916, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABWxGgW2vmwVTw+ziFHWUqG20hZypZ9Nr0gYJNUU9n4VMABqIzSdh/4xdKGAK5lwA+KU8j+KMiI2C8o="] -["send", 85053425905923, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEnY0gJTSJ/Q06U0/hD3CPU0s/7ceoKI3anYqhKF5wiltLQZBxbCtUeb8q1u4rEW3wkXdxs/8e2m/B0xMrBDwaK0QaZ4sdTFvc1ahOtbXT6BlijRVMAl4dsBYGf80+dTdIEccVaNags7WoNZRTm0uKyMCwi34nfou24R1Pd3xG/NcfSzufrI6Jruzp/JjGW+pxDf2CYXR7vHkInunr1tOUteo3iBdR9+9txXaOP45NkuAzK86Dx7ttxXWNHZdRooFUqi3NPvwQKwWbIrV7wbC3oom64n+KYyKOyvllWjjMVKj4H4b9KYWHjWHE0KHhcWmEW/vHOmg4OYStwWV3shshQQZTvB8h7jDPZyugStIdXIjHYyR4at3nqgPCeHnxCMxXUGL9E4KXlIuYZVCetzsJxKWkirpyVofT3TGu4MC7Le2dgZk6F7tyuH3N1z4sWkBHEG7GoFgFk0BnKawXsrX1R54fKd5kYeF5szC9Vw7fKCzl4F+tSQzqU7d4WU2CTj48IpMWYqb6VVbtdddZbEM09M7V7mpoymqp/hs8qZ4GDoMN5kmmxcrZG4ul0tYPimLDzCxZtYN8NuoKGQ6G9ijOatKwSrDGWmbA/e+w4x6WkcgcyjZvIf5JFLHNTBXoz6B+stXnazCYwexHhpczoNr6yd+C/j/T+i9IMhtyDHP5Ndo5DCjucibUvVNORS06Njtg1PEkbeC4nF"] -["receive", 85053426820477, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABaxGgV6PByDTtT69iDzPY0JujHRyUy8C6DGnHmhJQ=="] -["receive", 85053438963242, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABexGgWPO/Fv5jmgoZTCaqdnAQR7GsVARGmljohhe3kqB5llwkR+NHn0lL9n0SKFS+sgncjKwr/6H4s="] -["send", 85053439307992, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AErY0gKAk358zF8Cidd+uF0BMjIY4u1mXapmC2dVsFnvJgau9rEhX8v0wPBaIdp8HE4O2gm8K674NOm2j2JgELnliHD7DRVDor+kXIeiF03Bp5PQLOrnucDnA/WHf4pHOieo0ysYeXZiT0tb4WsqetR74i3xeu+Sf0CfX6TwXa/9OIuptxMsTVdGGma6cHMfLXkYDuEw650immWca5r511dQSlozJL8ty88I1qs70hwF9h8AqQ85RG7gdNLirfyR+rr/XNmaqEekcgkO8zxDLAI4y02045P8x8oIkyConENZlc6zQ0e7M8y+LNuMGdKnFRZ65o2L97r9GQjhqOBo/wViKAP+kiTrTD368j1bmE3dvrs1O105rN3loPc2biF3o9hFk+jz+HpjGN15D/K5E7wMPaNpPkZ6xTWbL0kJg4fqB3KcXF5qkdoQojeQzmKJO057lQbiwbmzpLJsFgruhW4kdrAxMHI8huuLMs+1E+8Iu3JOT6519u11z6sgTxHZ10hGAzZ4JsDtl0NfZP0wqVFhkvaO7MA0ICmzbWDi0dQugrfe1IVaPBGAl3xQUdBfpicu5Tvui9x2naFqtn///chx6rg1tnTmH5HbX/EL0JEOZndcRdV3hW8jJtmSI5wwgQ9YlWQg1mo72DS0Rw+mRr/pq1DdEOTZEDoehtqVZililfT6lWZF7cm9GnyN/A5msDMaus3+ce9mtdRWuWsgeERgpq8MG0OaePFiqTBp"] -["receive", 85053439340473, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABixGgUdX37zDgK4JK+Hp7v6Ltq9ba6TYXY+FOFRFA=="] -["receive", 85053448066465, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABmxGgUkvnGmhXIt8qwZbra+JtkbamXW38UXbJ/5kOeQR6Gs1SPvNM9TBZhFT3NsqjfUsCnglCh53W7z1pNTNt28WEau5AlKwIpJjuvSMIvPumGOdKoIm58gTg=="] -["send", 85053448948157, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEvY0gLsWnJK+T20soEJvjkM29FCsOCwLll8MGD1rniuNoUEjhT+V0+tryWlpZEZwtjuE16PmxyRZI1/8zvHbp+D4jzJNPPm85NTYhkMniIea+ONHkkNbRf6iq/X/0EAwjOPS/2J5Zu7prMLGkPlTwITegYza3UuwhSC/8y6nH9EmofJAiku2ToLgM9aiS2WILVu41BtYsP8FbFhZZq+dnseCdBpMTkjtoNwANvfnTIpR68KUTVXiDNALawf+yW2HgU0oMwzZ+yFjltNubTl9L7A77CbtnrNmPo9iRcwANYQyivPz6VbMY2R1BcUJNG1NLQD7NFBH0jetAfci3D28yM1tHRgY2o2RCvOvk1QF0Ku7a2X4WYBBgKfWjmbcIWF3ANFK5Z8py4iu30msGn4UYg9xv2//lPvf2OV75lrrS6Y+D0G9jQXDFaj42FnvdJGrQKAS2oz2g0L8bgC92FSGbPdRilC7abUdVDTnOb/tQjYHJ1eHeG8bM/CSnqKMAD6y+ksLdPmz/9R+Mj87OtS4KzsKbBveILZxXOH4gc="] -["receive", 85053448981851, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABqxGgXH7RUFrtoZGhM5GgnUWiqXM/namJZY8/t8vw=="] -["receive", 85053504624532, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAABuxGgWM2n5OoEOiMUezkXfD+Y7hmopN/K794XFoc9bD6gQE3LnLeSg+8bb227i1L3ipjU9CLpn09XNO4D9f/mdyKrTfBkwaRwETHkHniR6VRgf3WuCsrwYcUg=="] -["urandom", 85053504807297, 33, "4Lcgq+4mQ6YDk8kZnw/V6TH6nQuWDsOpY4IUevIBAIAW"] -["send", 85053506399438, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AEzY0gIkSsGvG8vpMCNOxETUzcMElQP3Jqi5x6MuyxZ8xEWFymxLNbIjGSxHuk/vncfVIyUSOvmrA9/6ySl0AI5mtocn3ORmtdLlAwynSQdvn9r608JOmqO3UArSbSo69rxWPrvcet6cVCKEYEOmw+iN2d/VSfRebtyr1wcO3nvS66XCOF81GERt8o7RF+Mh9dnqeDAeE89ZHN4O4fD8plfqH210MooW1zM1Mb6xtGK2q7NUncWtJrIQjejhCKF3snwwAB73c0cuUS6q3lntwRVWr2ZqAPhqUwTX9nfCbBI++NVj90FtvzYuiyPBRMyQ1Wazn+LpGiicrCQeoRE0kFCWKVKhHDhKcioWjWgPDGevBH7/g/erp7DRy3Met0F7p9e4CLzPCzLEjmO7181i1LKovmImubq+mnl9MJo+fhajmoEuyb6d7n3Olh37ixN3aZnBsYd/d+VlVf7KrK3xjLVGtRowHaqMORAlpvbxUkkjDJSArTEobVwdAG0wL43njneUQOro2uKc"] -["receive", 85053506479259, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAByxGgVhkBbPgPQ+uFr/gFeiFgUiY19L1up/aigPqQ=="] -["receive", 85053557026402, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAB2xGgUVcStdX3Eb5qU0pIGHMTVJDa4wfXBs3UbTY2rnLo2hZwUdF4/dHOmOeKTCguzUw8dr82aNArSqzItMlyTw6MPjFweAQOfDBqoiEf7MmtvT+y/bhkFsH0dL6cMwbxjPPxkPsElkjUFRQ2oK59UpNbyOBwPUlJGmHLA7zVxcSA2x8FQOoym/dAVKPN4+z+yIymFEfVVwpKHoNSd5Z6k9Q6nCEOwO86C/9ThIfD2Ulou1Zyn7ckTTos9f3WnIf4A3FhYad+qG2Aviunt0cZULq8Q80kfSeqoxWn+jrPHSK2OiFJoXXobNCfU4pJ9WSOsP0N3WAbKwMJoxlPwy+TSYZLlJ4LC8U2lH9K9bulPA9EQKvW6UgUD0g3LnOM71T5VCqPRVgBgJPRhrSfVwqwDASg=="] -["send", 85053557362516, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AE3Y0gIM2TTcNqd2vdFc6qDuH50CdsvIlrmd6jS70xbz1v4SkP+aLoPBNZSGW6YYtaPUbsk5n3BTAX7DQidxfQ=="] -["receive", 85053557396390, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAB6xGgX+E04cAQa9itUq7yImUSIjmVRlwmA76JAFFg=="] -["receive", 85053589209130, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAAB+xGgVJ4McbX0mCecKh1PzbyBoK3x0fTcK/pqS0Dxeg+K7ejd/C873MJtZU0fNQDGGfZFDBduXNrG87irOATEcTpLvPxgJKF93+qX2YCICEuuA70QxP19FO1UcsGzeR/GWuDEDh8NkpF267UKX8b+OAJ0WZ0iXk/Ehmdv6PpiLnfyl+qulyjf6tyv+NX8466H5pgv/aU8sEFcAA+WhQpVtRrffMlRhYkBLR6kPtHfyzfoN/Y3eie4VihGZTJWaeYOpmvIdqWVrspet2/NhDrs7M5RWRfuOSemuK++xFcUpzQessg6MDT+CSYDPUw2z8OZ0DvfuThGnMgbyOVaIGXY5uNolgen9YIsqi6LWsYYsIboTxFSgnlTZWfPCyaoeAaf36Zls0eoiP2IKt8MiGWJ3aHzDhqtA/N/NHEQt6Kv637AnzlFD/KIiNT0sWwKzMK7iObsGB3g=="] -["send", 85053590087597, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AA88AE7Y0gKyCxk6ujVQR/uaFAMFDFJMZxKMBKuN1qf3zQBvhBCAPD1wKvnPoDPy50V2wo97iU8Qz3YPf0lNGxCVwg=="] -["receive", 85053590125598, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAACCxGgVxCdxgV/VXIxwGJwDNPfrqkWACVXpYr2+x8g=="] -["receive", 85053595767866, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAEAACGxGgXO+XvwZiCACbOuPpUfUGiYfP/NLyn5ObVBlA=="] -["receive", 85054718839082, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADRBag5C1agNl2G7DAUw7ekAABUwASAtDq6lTH8/67aXJNcNKjvV3caTIiTpXpdzFZPB8CUppyUCEDwwAyCQplNI4rtpFNU2UkvG6/MhXRsO97IM0h1zblk4sjmLDDAEQQRc/w/paBjO5Uqttmk2uSwGw2hoH0ZMdsNOicVSw4IPzK2YCZluC3Bfa3OgBCRXLIr12jQkzEQIe8TQsLgfPiNmNQUlAfQBJQIsASUDoA8kBBEkBQsmBgAAAwEkBwEYGA=="] -["urandom", 85054719085587, 4, "IUE+rQ=="] -["urandom", 85054719103491, 16, "uzE8tKETTC4RwfLKW/smZw=="] -["urandom", 85054719120232, 33, "a6jfE33LC5iwrg4YQn4UI4cDDRjm6WnqmvN8ZX6jOX3h"] -["urandom", 85054721251822, 32, "WLiUijdVyR4f5jTi/S6l8srwc1ctCtIF2946+x50w8Y="] -["send", 85054721625817, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AQAAAEPI+wtC1agNl2G7DAYx7ekAADRBag4VMAEgWLiUijdVyR4f5jTi/S6l8srwc1ctCtIF2946+x50w8YkAgIwA0EE1ISZ4BZ2lyTgfvc4ja7L6ZQkwPyWKiKs59KN5degPYlG7FSk89NQ9ZX5fgGm+hRckc6lL6sw7vuB1P2ECCHoXTEEYwE6rHCryBV/Bkx2EUx2FKTyTkOFN0ae2GVqpT3eoNVdTeOwEQBt1EnMCJSbAMbf/geVqTgn6WckvgGwjRI4eeQ9t4WbOfPwubKUDFIxov3+vRX2tOzUG+jpCxP4JZyaYRIeLSRUP5UrFRu/enmjAXLkom5jsKbB4bVaLjRiXxqRAEOZ5dI7zSyyFDgmtRk8uRJGicfnGgUnP0Y26+QpLy4QkDOJVgGGOWnAA429xUoNsAfevt1o6pjOACrdaU3EeGBNHk3yesX8wlWXvQmbGB1sDcf2oAc4p3r15uBdutJ0NS+QTNZLH8FGThdd/OPsjxFXKAaL+Oh3FIjp1by8bH69VhCnhie8Fj07YcBZyjvQuNmQXsE2GnG+qVFcsFYOaEEX/w5+NErs9vHmzuBPm2eUHjyetcTrjs9fpqctLW9y5Jr6/JyVAosc+9iPqTK8Si8LLvFk7YCUWk2s7X80bNPplpVcGA=="] -["receive", 85054773247245, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADVBag5C1agNl2G7DAcy7ekAAEPI+wsVMQFXAX4MhFl217pgCraCVaOX0QjH/vhYFbrsMVih1sGaHxfmtpMzparjd5z+c2L7IvYcS/zudPqFMZFVZmC1oPSJiPaV3l82mRluHl13kHRnw8aOXDwSKsNc6JI00Q5gzqdrSb9NtiewmfF9gRObcxfxLggHjofMKqyTJo7F/igzqs3M+NgQglPJaW5n3a2jdJAJUBXY7Ah9LImKPySSDWMHDTM3F8XbP9aQWy5+v1+PhCpHoMlZmX/xkNYHFCfTDluWrRGc9zUfLaXKmsc+naV6S78NNvuvGvPZr9tXJYd5vZZvogZJdzWMtPkpzwkJd/m+V5FlJrbLWB0cq6Cto1gxhWRFylGOwpoiOxyIIq7TqmAeuFNpNY5p9YG91EH2k/a6+TSrlzcSd8MToMb34SCyaQsldN6Z0/rn9nvUL730SfHt3STwD8F1X5mELTzL48te+aNAYNkMOEMY"] -["send", 85054773596374, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AQAAAETI+wtC1agNl2G7DAZA7ekAADVBag4AAAAAAAAAAA=="] -["receive", 85054777448837, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAESvUAwC6VKjZo/eBGDIZ73vu6eeXxlQTZiKLEYo/v8IRTGBiwHnkhFgZDbHI0EYGhBBiARPmiw="] -["send", 85054777788147, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "ABA8ABTk0wrjfTC8S5o5MP8Vbzsbsj0j/t5rUqnaRRWowSIpH2+k8POKDiN1+/z1pSmdySUolT7KfJiPfaZeLK9dTHv0jQ=="] -["receive", 85054777841738, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "BAAAADZBag5C1agNl2G7DAMQ7ekAAETI+ws="] -["receive", 85054780292188, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAEWvUAxHobz7H6MO1UoE3RD1c1pZpkrzPSh1cbVxNA=="] -["receive", 85078301515948, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAEavUAzWQ5Inc6pjKJSyxib+r3nARZPmQq7V6Ztu6TabfDJaQimalLmJMBcMExqu"] -["send", 85078301974363, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "ABA8ABXk0wqjIdtACW/UdgX6tPhej18Wvzw0djzKsKqhugqDD/3bpCA7WadQuLuxXCj/2EFs5KRSgUCpqM6XjwWG/A=="] -["receive", 85078306805562, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAEevUAwl7Z8vKBl57/dsSC9JTqrOuKAZZXyB7F+YC33LVLxEGhaW"] -["receive", 85078322605591, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAEivUAykKrA9YurzzwormCtb60L+VrSBhFlTlcf5/vrakq81sG2/u6sW1MPlrX+s"] -["send", 85078322801090, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "ABA8ABbk0wo+Yhu+b1z4Qt4ONKeh2qka0R+GbSSezEGrC2M33tDDBEp3"] -["receive", 85078327002160, ["fd98:bbab:bd61:8040:1882:ef54:2c4c:fde5", 63102, 0, 0], "AAIAAEmvUAxtsgWRmFXRIzKGoePy9MR2al54Td3/ESA+X3akA/GRKceS"] +["urandom", 21894081008959, 4, "XvUqLg=="] +["urandom", 21894081027434, 4, "jrcU0g=="] +["urandom", 21894081038615, 4, "5BYc0Q=="] +["urandom", 21894081045118, 4, "lb9U8g=="] +["urandom", 21894087912061, 8, "xSEfVXmXq0A="] +["receive", 21916834359035, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAAK0VHgJE2rDcReREXwUg3ckAABUwASDvoeQNVIgYlkzJygJaPIIbh5eRTbASZTPEB1+nE+H6jiUCfxokAwAoBDUFJQH0ASUCLAElA6APJAQSJAULJgYAAAQBJAcBGBg="] +["urandom", 21916834794857, 32, "A40cHZ6Twissn5oM2oAq2zEeNMxghOgkFxFMlBb8kWI="] +["urandom", 21916834896429, 4, "aBDYWQ=="] +["send", 21916835122966, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AQAAAFev4gJE2rDcReREXwYh3ckAAK0VHgIVMAEg76HkDVSIGJZMycoCWjyCG4eXkU2wEmUzxAdfpxPh+o4wAiADjRwdnpPCKyyfmgzagCrbMR40zGCE6CQXEUyUFvyRYiQDATUEJQEQJzACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] +["receive", 21916877322779, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAAK4VHgJE2rDcReREXwci3ckAAFev4gIVMAFBBC0bfFkBLqjTZIZzL/hG8uiRFKOqxeIlrafh72iIWfiHVNtcdFvurASmAFyV0NWXphm6rjEIQqKscYeg9srtI4gY"] +["randbelow", 21916877475607, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 20635248899586279189889848880180462748155681201263863407999875450909239826541] +["send", 21916886924090, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AQAAAFiv4gJE2rDcReREXwYj3ckAAK4VHgIVMAFBBLNBzvrXQsAhoO/WLGfXFPg1q56sUy14BfyIdSX1cEIpT6o2zf1s/0RsIWTH3Cwi1XuZ6nN8XljyPiqztL0/ThYwAiA9XWXZPWKqh8vjctzVoqZgd9xQBLYgAYaMHgBmFsFHgBg="] +["receive", 21916899213712, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAAK8VHgJE2rDcReREXwck3ckAAFiv4gIVMAEgoKqGKIuUcRhY50dEvOERHkjhgVa6HXV9JVtOQdHhyfoY"] +["send", 21916899325944, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AQAAAFmv4gJE2rDcReREXwZA3ckAAK8VHgIAAAAAAAAAAA=="] +["receive", 21916902108142, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAALAVHgJE2rDcReREXwMQ3ckAAFmv4gI="] +["receive", 21916903034139, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAM01IAefjNJdWE1TQjI6ShhRFCR9A9NnAdBioptLb1+sp50eRVD7NzmlewzpRp9y"] +["send", 21916903424225, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAAiBnQUIjHqyXncBFI3yCxsDv/zx8fovewdAVQcBvQ+kgc8gA3M60FcNh3wzOysEt4ZSyRYwHFa9hgHbdNQLQSMg"] +["receive", 21916905859468, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAM41IAdJNKcBWzzvpiQ1/NzM95m6lC41JAPGKuKJNcQZGjHhjegt"] +["receive", 21916906873972, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAM81IAf/YWKZcBHjhAftMPXPWSL/PxIRn5cd7Yfc/SLy+P+t/Mu+w+3hnHM0ofLV"] +["send", 21916907129534, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAAmBnQUZFuHzojAT3j5l36T8YaTvlPpV4OBF1LffCJKzuteMiMmR9gDMQKw6QCu2W0FuqtmWNZWX7KVXsf9UMCOjBrIG4RnxTgE="] +["receive", 21916912200169, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANA1IAcHLA5Diiadb2uq1H5QJo33Qx9OiCAMu7/eDM+Rg49ougiL"] +["receive", 21916936270274, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANE1IAeIVfshh31R/QZpTwxbRHdMYFsJRV2YqwH6WQQzfdti0Cf2pjBX38d+jOHaww=="] +["send", 21916936526978, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAAqBnQXdQR0X25yyXVV/QXfLoPt9HFgA3bo6c852yXpvLHKEy8gB4EGYekYjOJhEn9hivCZfzNEPyVZoxDCH"] +["receive", 21916942315406, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANI1IAdZt3YVOpGaUda14CZm3uAyYfkNDN04TrhqCDqzWAcTU4G+"] +["receive", 21916957235290, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANM1IAf5nv49sHvmTn0KlfWGxPaGxdhchN2VX8dtuc5bJUGwmoBWhJVJXFTcBwh/"] +["send", 21916957482677, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAAuBnQVQihllEL2GWODvdI72ehXRgPw5NakuFcqlj2FeQPTWFxfURYojdHUM6ZC71orS6bph0KuOpTizrWbyTwutiutgaaqxBzCqzg=="] +["receive", 21916962740054, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANQ1IAd17MOYk50c+pDgLi3FE+tY64mZH6KkZ3it611qxj8lAEOq"] +["receive", 21916966325888, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANU1IAeuzV9TWJ3ZBKNNn9idGQ6Ki7jy9dc1uShyyD4Ime2tw7//NvrZemQ5yfcMmw=="] +["send", 21916966566983, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAAyBnQU4XtkC8nILeQqmSYlOJMp5jJOSR1UFPP/iV4Dl8TZHLDR/lIFUZsOwBVjKRi5M+jSZYR5R/6oQGKpM"] +["receive", 21916980238181, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANY1IAcrqOxnHxoZN3mlhq22MYQj67CueEgU2pg+0MYA2rNPZ+89"] +["receive", 21916980343430, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANc1IAf3dcHUzh2V3DONK5q3js2I5QRp1vAxFfUniJdOSnknQKfX7xnpu/Zi3rYk1vbrPUk/FVSCn+gB1YLPzLxLnnrFjoBVa12D6Ykie6p+kH2CRWbRAY3zEpYV6AL4ar2EuKnaQ7HxkfEopdN4ZDL1LIzgYDtn/qaJRc9fzCA="] +["send", 21916980949573, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAA2BnQW9/P2wR56RN1ORz1HvV4t33L5bnaE4Wz9sMW47rwP3pjTP/3KYOOBXdhQ19t8z0Cg9lyh4ts5VJVXzisrX7sO/DPmoPMYIIfkOnriEKbSNreN7f1+zaHQpmyeFOQaffg1mx1fVZQz9AZDKhaTdeaatBJCpIRCmsu7ALtT9ISY3jMPOZJNXnRwe7OeYSOJUjmiOPH5qdPmvgjvYBSdfztKq3jhKz4krWjp4XvAMM4XsNiR4CxyiuN9oPkrFHd+CUZypzilO3dbKUhuChO6MhUKCRK1ZWs1AHHVU4Z/DT0aAqrWBsGE="] +["receive", 21916984015887, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANg1IAcnCpmTkK41AjoLaIzH2qKReaaqHQ1QvDUTTOyfn8vqhKAE"] +["receive", 21916984735946, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANk1IAdA6f2Ntsmmp7u6cvzzztpfeCKwT3XIzwHZbjWw4HYW1LG29oZceM8vjbz7Bk+FPvdNQDSaQbGRViPIIUGeIVj5zvmDyvg8tghiNSMAPxyzbGEXs36zxRToB8jj7u4W/4gWhA=="] +["send", 21916985014431, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAA6BnQX2awWN2gsWyNLmjahaTg756tnbJqwgR+mbdjeIAFDFEZViwaTI2hpzidKzwTd9+gLAhqRn9D0Mcw=="] +["receive", 21916988392864, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANo1IAdcEx2bJb90tcXHTYfQhD2VCUM3zzcj3g+/jiY5dwAYGFa0"] +["receive", 21916992478340, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANs1IAd9TeCtsWI7Usx703uvcj3ZI8fBVJjKxX9JWcfrLXJ//O3vcsCr0TYJGxN2rq5Djq9CtbUfSiT/oQI="] +["send", 21916992800528, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aAA+BnQXvArFm7TTnoYT7Z/XBH83HwG3QKoTXrO8HtASls4DZ2htf1JrHYaQTqok67LYFNZDilScaA9+87XWZ8x/73w=="] +["receive", 21916997621351, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAANw1IAcg9OzS4A+qhIDGMkAqJlAt6xf6T2ka4qMxhDtPtnCJin8RX58DuqwAYxf3xhHVpLwMPACg14BO1ReLhLC2EQ=="] +["send", 21916997960271, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABCBnQVPQUhOg1sh9KI2HXtog5T1HY0u7EwoY6rlDLdD4c7d3TIclZvgFgj3ec8w4kPirkFHLqVlBR5BS/70/th8oQ=="] +["receive", 21916997990217, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAN01IAd0vYudmS40bU29FIKQYP22FLRSxU/8O8UpFw=="] +["receive", 21917004508042, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAN41IAfTp75ySdfePn7QM3FqQ/RrZ4OJSaCrDwRiYZdPDt9nlIctwo8a/H1L9s+PLSCv5Lf3DAEhQ8w="] +["send", 21917004857271, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABGBnQVN5fRZE0rF/odUjLcFLnMr2Snf0AnNRMXQuepyDr/5ypHOJCE6G0C69hM7RD9lzZ9HU5QQKrXG93v8JHMmf1I5jZSuZ5otSbeG+CHiW+qynHWpe7PIdns5UbtgCpGM1ywe70IgEvxqWwgTikc99Wu2rnt6QbtMzuICltRIyiBMZXxF5emtvCuP79hfSa/nJWMlj64x6TVp5Dq1h+KCdvgpHbbrRjXn7qlnE0xUXpVYQ0sYk4XT3BDpXmJjYvCQNjFfLKN9tOMKvFQGC9YqE/LzU1pCVlI91cNjmYLLRd2c8bTQOBsaNvN5ecMyn6XzUjlEN0+lenFn0pe7T9f1RrmTcM6vDzS8rlVZgWg+GLktfWbfrhpBixwX+DqwYWtqoXaQ97HL2hTw4g87piBTrTE1xWKYO4nQuuUB+xkWE615DqPhT5SxXpI11dmhzyF6y6vj8YeHjRywV1T6fXob6str0hbbpnH9NUOfNHhsWyn+uF7pssK7cP5UVN6M+MNxHAO1SZoJY3v3IUnmtmPxOKAxpPCSJS2bwAZJKU+//infnR2sLNnGC4lpGbyned79k/IC3QDxjX6sr1xzTzGdXRAEc97+81F1c1JoBZ9racBsoAFmwNMfkOqFT7g0iI5UsXWdmYMruN2pV/DGxzsPSlMI7HJdfc0h1P6dlxQk7GFg4FpwR4kVO1BZtGpgScMjqN3/1isy"] +["receive", 21917005802444, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAN81IAcwCt/X2/qKxI7rGuV0nA2BVAiJTw8vw3JRZQ=="] +["receive", 21917081678121, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOA1IAdFBBEqmCflh9OSxGllMpKyXqa0PHXIts/0y1rm/RX0DEqhHlBexX4r9at79Hwhdtc7uHyKo0Y="] +["send", 21917082032179, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABKBnQXQngEyGhxxYGtWjmEffjA58BAdXigsirbhsxnH9H/SmWKKjiFC29eytKN5KcYiOIgkfUxQU+rbvvHhgLA2M5XTNKyNTUk+UyMcvAUiNkdJ81VC0JGplUtrMW2y1QhiXPNWHGWm0lDk+R1kPZ+2EBUkEiVbIuYbc1hc5cDcE0MsuJSc26lWFoX5e93LwLaUJ9LaImV/fgjlgPULtGy+rIPM46NCERn/PtYKUI57ba9bNHD5uQalDuh1efMvJAwC3SXtVMY9xfZ1V5npnawQyb3QYFNt2QqT5iDqjiKDrhQZgI8UCEMoIx3Tcu3faCkBLYsMlDOprbLEPDMbB88e283pxbbu2cc5dI82j1xPiE27v1GVuJC+mJzQea3THtl9bLlpBNfDQ6pD78ZeIr5/+SJRpaWZT9GyMir/7uD3u6HT+H4xvjggPEwan+fzgMptxkHqpS4QuyWi9YUwwfl+okWJxl1jIhto1+dkQb/rLJ52YkQXfNrrf3lZJcR1YPbaif0gWGckSKq6Fy7XyIxmn9snyJgVa10LQNy9Jak71ItqD/68vqoL31vnGHsS8qN0hPKw19tcXVtWZQjq/rThGtAJgKamL96WXdGoqaxRlgPrrs96SmYtI6sUECLJ8FtBTPD+ytbX+i37PC8UoyzSFbcHLNJKqpjU5XAA+EzxqazQ9v/re0NfStTpWTmYNGoLzpDmAFM4cetAfN/fiAlSxOzVJfYc5vMXiffg"] +["receive", 21917082098353, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOE1IAeUF7Nhh8QF7j84sl39cCzgPRkk7RDjKLJ9eg=="] +["receive", 21917611479279, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOI1IAdlvZAGyRsy76KqXEx72YYHRXaoztZMx9goVNp106bE43yL6GjvTCfCartNk4F5ClqtkjDNeowzWNfCU99fnnnbVQbDDcYVxYA2G6jBdimiJdQ8PLDm6A=="] +["send", 21917612493633, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABOBnQXqX5KAGwaFqWOaT4HYRmBFJJOPdP1XW4G+Fhaf8zV22J4SpUkNEXjAHCXDBcGuwLBsLRrsGmwLY7EmkP5pK4zmyJvJ4J0fGEovfq7z4j5VGVmF0nHL1GasQhhX2QCxUrQU84lqxKZ7X1Gmtt6OETGAKA/I0bCG9WguBKolqoesFyeRJk1T0yx55Zs4nVTjGTwd474mlBBVM4Im0Ej3XDo4iEHyZiKLW0/ZZ19rFnk8KLru89toYkrFuakpc5Hbx/D0Z9HGk//ExcCamCvsR8fWfo78UeOjZV1TELaocgcCa/l5eamG3vmE/+f+5ws3gkkCg8g9Uor3va85+d9ZeMiEh3K6s2w7LRaf0jDbqDIHlkbDLZGpMWgPVDTFN4InWPZNKh7W4mc/qHwzUraoakXjIy08v3vp3XzoA9G5rZbYoGTrE96yjUtmf2pNQW2RDHKmLQ1QnSy/m6+qkXim6fuET9P8NZrK4Uu6tbXVL3b9xutiXF2CrfI55496ZbZ+c2ZfZKPc9N8hdvE3oO0u5MWYXBjIGtAdqrg="] +["receive", 21917643028893, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOQ1IAdlv2wZE03/0J9c5Pjbzw+eWzACZIrGYncw9R66krdwjBp+O6RTGqzRjvjkC4MjIuIB6N7O3lBbTeU="] +["send", 21917643428216, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABSBnQWJeVhwZWI/dGMq5GSZYNjNUSyclBSmBdv52iC0lCZoYORugqTorCNgOpBZoZNr/LWorpXJGLShMznjN6cSTQ=="] +["receive", 21917643458603, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOU1IAd+/Jt//2BKCt0xD4vDn2t6Bnx0Y7G8gpeGRA=="] +["receive", 21917650527507, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOY1IAehwkagzV3zc+OCUX9IUxCoKmTnQ9GlJpY05g=="] +["receive", 21918903683973, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOc1IAdsmaP1oUbvNA1pHNp9AGdUre9QRdS7WUHnrv7314rHEb31GvUu8ZQW+Qjc1tkXRuHjNHR/ELLmfTB3uqkoz8amh7lcGhl1SH4RWn2voRJ29i8ti8rJig=="] +["urandom", 21918903999829, 33, "4IKkt0i5n/JSrVQOQqCyQbcMV3jISs53dU7Hs3gitUQh"] +["send", 21918905537429, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABWBnQUZoc6IX8af0Twfd6Y0QzoM/cUowtWqdtyrNBeM3iGIKNM9AUuMNCwXlI4HP6Fc+kOCReFWOGvOMM1jsUxIElFvUYZvOFi2sanKxAT8xgkQlGD2wsj7g3jaH7oe5U/44ylekitfdjWq+1/e3ixHDD2zQEh9u6023LVVQKWbcQBTzQ1EYyhwr2qnqnkf1pOzYrlq9GezVvMIPT+MfVIO9ghJBYBWOk7wH7zRLxsOPs4Le1beSmcJeEcTh4G31Mj9/bLLs9bnhUEJaeXOPwIUqedWEm7wNKzgfe7OU6G4SptHw+8LZpvluiBbCZLPwRJ8+wgtbOxNvFBs4fx6g9Y0dIRufwBa9oYPFUkwVAZo9YhFW9s7zAvpmaRZlwAO2/Llz+kDQL7FUkqHnCsklv6dXDjgrkzwgA2o0D8QLWSl4IvJXV1RdWZcRWa0bROPuF3BGTOnyANyT7q0AhW67qx6RmRrL1hXEuvg8EAlENWGq2W1Et+ixjqPYQkg7pSMkDQUxXbVj5uv"] +["receive", 21918909476630, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOg1IAd9uXprQnGzZb++x5XUZBk7aczRTzOGrPoGgw=="] +["receive", 21918912917540, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOk1IAckIIq2yCzjlVQ2t1Ja/2adQB53JL7OReLRrJKzzB6rbgtL7QkIY1zFUfJ9ubuEvMEIlqB49OIE1T1ZwbyjpbC9m2YXnROKNoCOikkI+KY9uRT8VVjZ4i68pGdwnzFz5PJsegjrLk4I7ySR2JAwaNqNTqGEvOns+yCqHAWeLz6mHNNqF7VWiNtBi3JrGHR/clc9GINV1wIMYCjCGg0tcAihahVmRwVa4kLF7giHRv1DvsGJyJkL7GP2BDv/PqlfY1VKrcRviKWhIvm+HLcS7gQVo6tJLeNrmmToYakKeTbuvOThAi7jQrW/ZcBk7F4izvGF96y4MfXExX/WlzIn4SZSlNEDOcjF9Vn4nFvX+D/iYS0ZDyK/9Bg4vqTdm4KUmU1B6CyXbVSQ7LCglKY="] +["send", 21918913214791, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABaBnQUzpbhTb6FNK9TOqPzODK+wEUrGHQpayAMzhtNsvFiQnPsi9G1wmth6APnthtRunw82GekkhDMlKkwBTg=="] +["receive", 21919022007470, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOo1IAd3NfJ7zftzGT0rEcqlVHqKTKUXC6r1z5/P+DRfHBnsSO0rHVDMFgQSPPP0ydh423LWKjN4NDiK70DYwHcoRlPNQWpbsfyp0UgX95wPfJGzrzGV5CZp7vmZUJPUnGPF83BlFau92o+3MAVihzLExRjr22JEZtT4Mxp2PFShZU4gBueN8YbUSpzY59Ahc6acfENPcTnm9Lp2pKxlHllC8lA9YAlHAWf73NlgY+EA5ompMVBUo5PO/7d1EiYPMkucJr1wqYYnF3ynL9hZEvrdHpWkUybfNW3oLyg9hpjuWxMvzDpqg8B3h5eQEpOS4W/v6X02RFj3qYo0DO1MFAra7NBy/DkS2NzBAVzlSeEyNKI2e81QDN32SDiCf3YeWQhacYczzZiIKF45qNAI32WwVVc/Alwqt0+vnAMZ29yhFlIR6WVYMCAEnXfWebj1P+NKM1ezp4A="] +["send", 21919022809533, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AH8aABeBnQXlct+XRqbzDs28acu+xEtMU009vLPb4ZeP8FJWBflNe/uSbmW35ONc1DsdKA5dVkMIL+Jhz7KqQ9dfHQ=="] +["receive", 21919022841864, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOs1IAevnSjN/fc7xHGSZqROWmUal722M+LWx3iBrg=="] +["receive", 21919031531485, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAEAAOw1IAd3lGR/6SPEfQk2M0JOyCIRgI//kozMRyPKPg=="] +["receive", 21923257339887, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAALEVHgIBgnf2oEXiDwUw7skAABUwASC1Leko1ydfqAnAGWd1LB5dde3zfF9H5W+yqVnBZgsX6SUCgBowAyC30ZqKf1VEz/rTD5IVGx1OCk+yepRpTMxLH4Lmhml/gzAEQQTYTuMMJFniUx4n2CRjDo++Vp7vtvvpMSU5oDD6NRBDipMG5gvKIcQKf6GPoE1yb70216UeeNNIfFpkMVq3b3ekNQUlAfQBJQIsASUDoA8kBBIkBQsmBgAABAEkBwEYGA=="] +["urandom", 21923257602192, 4, "bhDINg=="] +["urandom", 21923257618894, 16, "Zh8MX0XMu2sKWrZnCPb9EA=="] +["urandom", 21923257632359, 33, "nz9N1LpkCCcezlOhKTmfk2bTj5OcKczTAyC635atsuTt"] +["urandom", 21923259804106, 32, "wem6k6wmT5JeG7QgD8q/viiaWZ6zX7Z42NiItuPXV2I="] +["send", 21923260038959, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AQAAAFqv4gIBgnf2oEXiDwYx7skAALEVHgIVMAEgwem6k6wmT5JeG7QgD8q/viiaWZ6zX7Z42NiItuPXV2IkAgIwA0EE1pwmnq0NYmthwUFXPhrem1sJ0fyZUA/EkmqmFOKQSv78HwfOVyyqKiYkun4sj2HPtXefNfdO+VK/B51kzPB4RTEEaAGoWFQag+HBZkk8poG0pv1CVQIkGBGTCmV1k8fULgN8e7+st7cAhWlojBpObM4NjRmH51q/f1Tu9QNTpnOkwMoFJAJNhQ9Q1WhW57X9iGTIuID3JUnWyJqavc/ALfVHREK7Wx7CKmSH32YTb5xn2nUFFmdjzoVzT4/wqWMoLb5weyIxPWO1SnhXb9PAFIuwJ17u4rX2Gx6ahrUu5hc5fRPpkG9n1hOUFDVAh7bfxhJsdyBjvcGiVOyuwpqpQJHYgqrETVsGgiiRy0i7OlOWBpLHOkFwyM5L5W6mgpg20sMihn0j8eUWjtz5SDhnFTePc3mQMfUYJryJBCxjZ9FBDYF+Fj9DRDxo9MWFYHu5wM5x58Y3ZOO9v63eRmP/X2l8JDE+z15QduvlBMtVQlIhNXoKqDCzF95xDUoCRGfzPWAiCV+1e67w+2vd3KxZqsIx88S0Pr48ez7qEEGc+hE9J06diE7YgkVoH1wY"] +["receive", 21923408064894, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAALIVHgIBgnf2oEXiDwcy7skAAFqv4gIVMQFcAeUKu1jOPc8C5BS8QTGKU7Ab0mGOsbBEMP2ibV/EfBJruyjh0OWGKYkAHnZozivguO8szs6vDRIjBMO38EC2LusbTO2VCcRnaD0WNqaiYMk3VqiYf9tHUcybo1dDI9Q7FFLUn2pupo3ufsGnFLdbokp7+Sfr2xL2sqrYpNvB22F/EPnsN9PgZDiQqubowiPVgIphCK8X3oaqfU0TRfipPrVYuIbsLtMvjN7CTN0ywPYjp3okA4RrvT8gjkBiK20Mi87nYouNltfB++/KpNkGGkP5mGT2AY0BE9Af53kHyY5SDQ/9EWK1zHQMjBVKbNWC/SMfSIT9/waJpBboACLH/WzfBlViwK0JB+gVk5O9dkBZQ5VelMnYby97uchpwYBwCV4DNOrFrzRimvXS+FG9kBThRwNpuvts/O+Wgz97UMHRb2aI4OEQGCReCx2kxzfhwIQFSHP+UK9nEWxK6Bg="] +["send", 21923408330535, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AQAAAFuv4gIBgnf2oEXiDwZA7skAALIVHgIAAAAAAAAAAA=="] +["receive", 21923537684809, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAIAAGZzDgekU2buw9XUtZc3n+EXoRB9rZ2WECU4C/lui3JSWWjV3cPQmTKPExI97n7UgMHiIubRJ5g="] +["send", 21923538115031, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AIAaAAiBbANvZkVtb3sHn/LUaE8UDUa27J4L6Qq8Hwwz93qDNPc3O54vI4J8MsMUVDcpCFQFiQN+tEqcUcllyTkBJL7jeA=="] +["receive", 21923538146170, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "BAAAALMVHgIBgnf2oEXiDwMQ7skAAFuv4gI="] +["receive", 21923545829794, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAIAAGdzDgcS+7HpsM/LogDODpSTXJLoB8Tq4+7e66quUw=="] +["receive", 21923568427410, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAIAAGhzDgd37q0GxjYNuiu585YmkourxDfN5g5yjDqNDjBnaHDfVs+GNdkVs7HrpIq8X9x+8U6bM/8l2mxU6fPthVdvxNvHZix6JgUTUsd/8a923rCSHHjXwS1DaFItM5VJqz7U8cDS"] +["receive", 21924043106741, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAIAAGhzDgd37q0GxjYNuiu585YmkourxDfN5g5yjDqNDjBnaHDfVs+GNdkVs7HrpIq8X9x+8U6bM/8l2mxU6fPthVdvxNvHZix6JgUTUsd/8a923rCSHHjXwS1DaFItM5VJqz7U8cDS"] +["send", 21924043262856, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AIAaAAmBbANIWBRgW/zrdjvYY8d/nf50Qz+t/AHJzKE0xw=="] +["receive", 21934027221255, ["fd98:bbab:bd61:8040:403:16aa:52f3:b037", 58428, 0, 0], "AAIAAGlzDgeHD1bPhjftySmRe/CKvsPiMsB1a4IOc0AzCNte0s9MhNU3quCIy514ehfsCJBh/mJl/w=="]