diff --git a/circuitmatter/__init__.py b/circuitmatter/__init__.py index df2e236..8b8e004 100644 --- a/circuitmatter/__init__.py +++ b/circuitmatter/__init__.py @@ -482,7 +482,6 @@ def encode_into(self, buffer, cipher=None): self.security_flags, self.message_counter, ) - print(self.flags, self.session_id) nonce_start = 3 nonce_end = nonce_start + 1 + 4 offset += 8 @@ -549,6 +548,8 @@ def encode_into(self, buffer, cipher=None): ] = self.application_payload unencrypted_offset += len(self.application_payload) + # print("unencrypted", unencrypted_buffer[:unencrypted_offset].hex(" ")) + # Encrypt the payload if cipher is not None: # The message may not include the source_node_id so we encode the nonce separately. @@ -892,6 +893,37 @@ def arm_fail_safe( response.ErrorCode = data_model.CommissioningErrorEnum.OK return response + def set_regulatory_config( + self, args: data_model.GeneralCommissioningCluster.SetRegulatoryConfig + ) -> data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse: + response = data_model.GeneralCommissioningCluster.SetRegulatoryConfigResponse() + response.ErrorCode = data_model.CommissioningErrorEnum.OK + return response + + +class NodeOperationalCredentialsCluster(data_model.NodeOperationalCredentialsCluster): + def certificate_chain_request( + self, args: data_model.NodeOperationalCredentialsCluster.CertificateChainRequest + ) -> data_model.NodeOperationalCredentialsCluster.CertificateChainResponse: + response = ( + data_model.NodeOperationalCredentialsCluster.CertificateChainResponse() + ) + if args.CertificateType == data_model.CertificateChainTypeEnum.PAI: + print("PAI") + elif args.CertificateType == data_model.CertificateChainTypeEnum.DAC: + print("DAC") + response.Certificate = b"" + return response + + def attestation_request( + self, args: data_model.NodeOperationalCredentialsCluster.AttestationRequest + ) -> data_model.NodeOperationalCredentialsCluster.AttestationResponse: + print("attestation") + response = data_model.NodeOperationalCredentialsCluster.AttestationResponse() + response.AttestationElements = b"" + response.AttestationSignature = b"" + return response + class CircuitMatter: def __init__( @@ -948,6 +980,8 @@ def __init__( self.add_cluster(0, network_info) general_commissioning = GeneralCommissioningCluster() self.add_cluster(0, general_commissioning) + noc = NodeOperationalCredentialsCluster() + self.add_cluster(0, noc) def start_commissioning(self): descriminator = self.nonvolatile["descriminator"] @@ -1006,21 +1040,23 @@ def get_report(self, cluster, path): return report def invoke(self, cluster, path, fields, command_ref): + print("invoke", path) response = interaction_model.InvokeResponseIB() - cstatus = interaction_model.CommandStatusIB() - cstatus.CommandPath = path - status = interaction_model.StatusIB() - status.Status = 0 - status.ClusterStatus = 0 - cstatus.Status = status - if command_ref is not None: - cstatus.CommandRef = command_ref - response.Status = cstatus - cdata = interaction_model.CommandDataIB() - cdata.CommandPath = path - cdata.CommandFields = cluster.invoke(path, fields) + cdata = cluster.invoke(path, fields) + if cdata is None: + cstatus = interaction_model.CommandStatusIB() + cstatus.CommandPath = path + status = interaction_model.StatusIB() + status.Status = interaction_model.StatusCode.UNSUPPORTED_COMMAND + cstatus.Status = status + if command_ref is not None: + cstatus.CommandRef = command_ref + response.Status = cstatus + return response + if command_ref is not None: cdata.CommandRef = command_ref + print("cdata", cdata) response.Command = cdata return response @@ -1180,8 +1216,6 @@ def process_packet(self, address, data): elif protocol_opcode == SecureProtocolOpcode.ICD_CHECK_IN: print("Received ICD Check-in") elif message.protocol_id == ProtocolId.INTERACTION_MODEL: - print(message) - print("application payload", message.application_payload.hex(" ")) if protocol_opcode == InteractionModelOpcode.READ_REQUEST: print("Received Read Request") read_request, _ = interaction_model.ReadRequestMessage.decode( @@ -1203,6 +1237,8 @@ def process_packet(self, address, data): # 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.append(self.get_report(cluster, path)) else: print(f"Cluster 0x{path.Cluster:02x} not found") @@ -1214,6 +1250,8 @@ def process_packet(self, address, data): print(f"Cluster 0x{path.Cluster:02x} not found") response = interaction_model.ReportDataMessage() response.AttributeReports = attribute_reports + for a in attribute_reports: + print(a) exchange.send( ProtocolId.INTERACTION_MODEL, InteractionModelOpcode.REPORT_DATA, @@ -1225,13 +1263,7 @@ def process_packet(self, address, data): message.application_payload[0], message.application_payload[1:] ) for invoke in invoke_request.InvokeRequests: - print(invoke) path = invoke.CommandPath - print(path) - command = "*" if path.Command is None else f"0x{path.Command:04x}" - print( - f"Invoke Endpoint: {path.Endpoint}, Cluster: 0x{path.Cluster:04x}, Command: {command}" - ) invoke_responses = [] if path.Endpoint is None: # Wildcard so we get it from every endpoint. @@ -1267,3 +1299,7 @@ def process_packet(self, address, data): ) elif protocol_opcode == InteractionModelOpcode.INVOKE_RESPONSE: print("Received Invoke Response") + else: + print(message) + print("application payload", message.application_payload.hex(" ")) + print() diff --git a/circuitmatter/data_model.py b/circuitmatter/data_model.py index 44c5d58..9e36a76 100644 --- a/circuitmatter/data_model.py +++ b/circuitmatter/data_model.py @@ -1,7 +1,7 @@ import enum import random import struct -from typing import Iterable +from typing import Iterable, Optional from . import interaction_model from . import tlv @@ -131,10 +131,6 @@ def __init__(self, command_id, request_type, response_id, response_type): self.response_id = response_id self.response_type = response_type - def __call__(self, arg): - print("call command") - pass - class Cluster: feature_map = FeatureMap() @@ -159,7 +155,10 @@ def get_attribute_data(self, path) -> interaction_model.AttributeDataIB: for field_name, descriptor in self._attributes(): if descriptor.id != path.Attribute: continue - data.Data = descriptor.encode(getattr(self, field_name)) + value = getattr(self, field_name) + print("encoding anything", value) + data.Data = descriptor.encode(value) + print("get", field_name, data.Data.hex(" ")) found = True break if not found: @@ -173,18 +172,33 @@ def _commands(cls) -> Iterable[tuple[str, Command]]: if not field_name.startswith("_") and isinstance(descriptor, Command): yield field_name, descriptor - def invoke(self, path, fields) -> bytes: - print("invoke", path.Command) + def invoke(self, path, fields) -> Optional[interaction_model.CommandDataIB]: found = False for field_name, descriptor in self._commands(): if descriptor.command_id != path.Command: continue + arg = descriptor.request_type.from_value(fields) - print("invoke", field_name, descriptor, arg) - result = getattr(self, field_name)(arg) - return descriptor.response_type.encode(result) + print("invoke", self, field_name, descriptor) + print(arg) + command = getattr(self, field_name) + if callable(command): + result = command(arg) + else: + print(field_name, "not implemented") + return None + print("result", result) + cdata = interaction_model.CommandDataIB() + response_path = interaction_model.CommandPathIB() + response_path.Endpoint = path.Endpoint + response_path.Cluster = path.Cluster + response_path.Command = descriptor.response_id + cdata.CommandPath = response_path + if result: + cdata.CommandFields = descriptor.response_type.encode(result) + return cdata if not found: - print("not found", path.Attribute) + print("not found", path.Command) return None @@ -269,6 +283,12 @@ class CommissioningErrorEnum(Enum8): BUSY_WITH_OTHER_ADMIN = 4 +class RegulatoryLocationType(Enum8): + INDOOR = 0 + OUTDOOR = 1 + INDOOR_OUTDOOR = 2 + + class GeneralCommissioningCluster(Cluster): CLUSTER_ID = 0x0030 @@ -276,11 +296,6 @@ class BasicCommissioningInfo(tlv.Structure): FailSafeExpiryLengthSeconds = tlv.IntMember(0, signed=False, octets=2) MaxCumulativeFailsafeSeconds = tlv.IntMember(1, signed=False, octets=2) - class RegulatoryLocationType(Enum8): - INDOOR = 0 - OUTDOOR = 1 - INDOOR_OUTDOOR = 2 - breadcrumb = NumberAttribute(0, signed=False, bits=64, default=0) basic_commissioning_info = StructAttribute(1, BasicCommissioningInfo) regulatory_config = EnumAttribute( @@ -295,14 +310,29 @@ class ArmFailSafe(tlv.Structure): ExpiryLengthSeconds = tlv.IntMember(0, signed=False, octets=2, default=900) Breadcrumb = tlv.IntMember(1, signed=False, octets=8) - class ArmFailSafeResponse(tlv.Structure): + class CommissioningResponse(tlv.Structure): ErrorCode = tlv.EnumMember( 0, CommissioningErrorEnum, default=CommissioningErrorEnum.OK ) DebugText = tlv.UTF8StringMember(1, max_length=128, default="") + ArmFailSafeResponse = CommissioningResponse + arm_fail_safe = Command(0x00, ArmFailSafe, 0x01, ArmFailSafeResponse) + class SetRegulatoryConfig(tlv.Structure): + NewRegulatoryConfig = tlv.EnumMember(0, RegulatoryLocationType) + CountryCode = tlv.UTF8StringMember(1, max_length=2) + Breadcrumb = tlv.IntMember(2, signed=False, octets=8) + + SetRegulatoryConfigResponse = CommissioningResponse + + set_regulatory_config = Command( + 0x02, SetRegulatoryConfig, 0x03, SetRegulatoryConfigResponse + ) + + commissioning_complete = Command(0x04, None, 0x05, CommissioningResponse) + class NetworkCommissioningCluster(Cluster): CLUSTER_ID = 0x0031 @@ -363,3 +393,115 @@ class NetworkCommissioningStatus(Enum8): supported_wifi_bands = ListAttribute(8) supported_thread_features = BitmapAttribute(9) thread_version = NumberAttribute(10, signed=False, bits=16) + + +class CertificateChainTypeEnum(Enum8): + DAC = 1 + PAI = 2 + + +class NodeOperationalCertStatusEnum(Enum8): + OK = 0 + """OK, no error""" + INVALID_PUBLIC_KEY = 1 + """Public Key in the NOC does not match the public key in the NOCSR""" + INVALID_NODE_OP_ID = 2 + """The Node Operational ID in the NOC is not formatted correctly.""" + INVALID_NOC = 3 + """Any other validation error in NOC chain""" + MISSING_CSR = 4 + """No record of prior CSR for which this NOC could match""" + TABLE_FULL = 5 + """NOCs table full, cannot add another one""" + INVALID_ADMIN_SUBJECT = 6 + """Invalid CaseAdminSubject field for an AddNOC command.""" + FABRIC_CONFLICT = 9 + """Trying to AddNOC instead of UpdateNOC against an existing Fabric.""" + LABEL_CONFLICT = 10 + """Label already exists on another Fabric.""" + INVALID_FABRIC_INDEX = 11 + """FabricIndex argument is invalid.""" + + +RESP_MAX = 900 + + +class NodeOperationalCredentialsCluster(Cluster): + CLUSTER_ID = 0x003E + + class NOCStruct(tlv.Structure): + NOC = tlv.OctetStringMember(0, 400) + ICAC = tlv.OctetStringMember(1, 400) + + class FabricDescriptorStruct(tlv.Structure): + RootPublicKey = tlv.OctetStringMember(1, 65) + VendorID = tlv.IntMember(2, signed=False, octets=2) + FabricID = tlv.IntMember(3, signed=False, octets=2) + NodeID = tlv.IntMember(4, signed=False, octets=8) + Label = tlv.UTF8StringMember(5, max_length=32, default="") + + class AttestationRequest(tlv.Structure): + AttestationNonce = tlv.OctetStringMember(0, 32) + + class AttestationResponse(tlv.Structure): + AttestationElements = tlv.OctetStringMember(0, RESP_MAX) + AttestationSignature = tlv.OctetStringMember(1, 64) + + class CertificateChainRequest(tlv.Structure): + CertificateType = tlv.EnumMember(0, CertificateChainTypeEnum) + + class CertificateChainResponse(tlv.Structure): + Certificate = tlv.OctetStringMember(0, 600) + + class CSRRequest(tlv.Structure): + CSRNonce = tlv.OctetStringMember(0, 32) + IsForUpdateNOC = tlv.BoolMember(1, optional=True, default=False) + + class CSRResponse(tlv.Structure): + CSR = tlv.OctetStringMember(0, RESP_MAX) + AttestationSignature = tlv.OctetStringMember(1, 64) + + class AddNOC(tlv.Structure): + NOCValue = tlv.OctetStringMember(0, 400) + ICACValue = tlv.OctetStringMember(1, 400, optional=True) + IPKValue = tlv.OctetStringMember(2, 16) + CaseAdminSubject = tlv.IntMember(3, signed=False, octets=8) + AdminVendorId = tlv.IntMember(4, signed=False, octets=2) + + class UpdateNOC(tlv.Structure): + NOCValue = tlv.OctetStringMember(0, 400) + ICACValue = tlv.OctetStringMember(1, 400, optional=True) + + class NOCResponse(tlv.Structure): + StatusCode = tlv.EnumMember(0, NodeOperationalCertStatusEnum) + FabricIndex = tlv.IntMember(1, signed=False, octets=1, optional=True) + DebugText = tlv.UTF8StringMember(2, max_length=128, optional=True) + + class UpdateFabricLabel(tlv.Structure): + Label = tlv.UTF8StringMember(0, max_length=32) + + class RemoveFabric(tlv.Structure): + FabricIndex = tlv.IntMember(0, signed=False, octets=1) + + class AddTrustedRootCertificate(tlv.Structure): + RootCACertificate = tlv.OctetStringMember(0, 400) + + attestation_request = Command(0x00, AttestationRequest, 0x01, AttestationResponse) + + certificate_chain_request = Command( + 0x02, CertificateChainRequest, 0x03, CertificateChainResponse + ) + + csr_request = Command(0x04, CSRRequest, 0x05, CSRResponse) + + add_noc = Command(0x06, AddNOC, 0x08, NOCResponse) + + update_noc = Command(0x07, UpdateNOC, 0x08, NOCResponse) + + update_fabric_label = Command(0x09, UpdateFabricLabel, 0x08, NOCResponse) + + remove_fabric = Command(0x0A, RemoveFabric, 0x08, NOCResponse) + + add_trusted_root_certificate = Command( + 0x0B, AddTrustedRootCertificate, 0x08, NOCResponse + ) diff --git a/circuitmatter/interaction_model.py b/circuitmatter/interaction_model.py index 40ac840..7221d74 100644 --- a/circuitmatter/interaction_model.py +++ b/circuitmatter/interaction_model.py @@ -1,6 +1,69 @@ +import enum + from . import tlv +class StatusCode(enum.IntEnum): + SUCCESS = 0x00 + """Operation was successful.""" + FAILURE = 0x01 + """Operation was not successful.""" + INVALID_SUBSCRIPTION = 0x7D + """Subscription ID is not active.""" + UNSUPPORTED_ACCESS = 0x7E + """The sender of the action or command does not have authorization or access.""" + UNSUPPORTED_ENDPOINT = 0x7F + """The endpoint indicated is unsupported on the node.""" + INVALID_ACTION = 0x80 + """The action is malformed, has missing fields, or fields with invalid values. Action not carried out.""" + UNSUPPORTED_COMMAND = 0x81 + """The indicated command ID is not supported on the cluster instance. Command not carried out.""" + INVALID_COMMAND = 0x85 + """The cluster command is malformed, has missing fields, or fields with invalid values. Command not carried out.""" + UNSUPPORTED_ATTRIBUTE = 0x86 + """The indicated attribute ID, field ID or list entry does not exist for an attribute path.""" + CONSTRAINT_ERROR = 0x87 + """Out of range error or set to a reserved value. Attribute keeps its old value. Note that an attribute value may be out of range if an attribute is related to another, e.g. with minimum and maximum attributes. See the individual attribute descriptions for specific details.""" + UNSUPPORTED_WRITE = 0x88 + """Attempt to write a read-only attribute.""" + RESOURCE_EXHAUSTED = 0x89 + """An action or operation failed due to insufficient available resources.""" + NOT_FOUND = 0x8B + """The indicated data field or entry could not be found.""" + UNREPORTABLE_ATTRIBUTE = 0x8C + """Reports cannot be issued for this attribute.""" + INVALID_DATA_TYPE = 0x8D + """The data type indicated is undefined or invalid for the indicated data field. Command or action not carried out.""" + UNSUPPORTED_READ = 0x8F + """Attempt to read a write-only attribute.""" + DATA_VERSION_MISMATCH = 0x92 + """Cluster instance data version did not match request path.""" + TIMEOUT = 0x94 + """The transaction was aborted due to time being exceeded.""" + UNSUPPORTED_NODE = 0x9B + """The node ID indicated is not supported on the node.""" + BUSY = 0x9C + """The receiver is busy processing another action that prevents the execution of the incoming action.""" + UNSUPPORTED_CLUSTER = 0xC3 + """The cluster indicated is not supported on the endpoint.""" + NO_UPSTREAM_SUBSCRIPTION = 0xC5 + """Used by proxies to convey to clients the lack of an upstream subscription to a source.""" + NEEDS_TIMED_INTERACTION = 0xC6 + """A Untimed Write or Untimed Invoke interaction was used for an attribute or command that requires a Timed Write or Timed Invoke.""" + UNSUPPORTED_EVENT = 0xC7 + """The indicated event ID is not supported on the cluster instance.""" + PATHS_EXHAUSTED = 0xC8 + """The receiver has insufficient resources to support the specified number of paths in the request.""" + TIMED_REQUEST_MISMATCH = 0xC9 + """A request with TimedRequest field set to TRUE was issued outside a Timed transaction or a request with TimedRequest set to FALSE was issued inside a Timed transaction.""" + FAILSAFE_REQUIRED = 0xCA + """A request requiring a Fail-safe context was invoked without the Fail-Safe context.""" + INVALID_IN_STATE = 0xCB + """The received request cannot be handled due to the current operational state of the device.""" + NO_COMMAND_RESPONSE = 0xCC + """A CommandDataIB is missing a response in the InvokeResponses of an Invoke Response action.""" + + class AttributePathIB(tlv.List): """Section 10.6.2""" @@ -42,8 +105,8 @@ class DataVersionFilterIB(tlv.Structure): class StatusIB(tlv.Structure): - Status = tlv.IntMember(0, signed=False, octets=1) - ClusterStatus = tlv.IntMember(1, signed=False, octets=1) + Status = tlv.EnumMember(0, StatusCode) + ClusterStatus = tlv.IntMember(1, signed=False, octets=1, optional=True) class AttributeDataIB(tlv.Structure): @@ -121,8 +184,8 @@ class CommandStatusIB(tlv.Structure): class InvokeResponseIB(tlv.Structure): - Command = tlv.StructMember(0, CommandDataIB) - Status = tlv.StructMember(1, CommandStatusIB) + Command = tlv.StructMember(0, CommandDataIB, optional=True) + Status = tlv.StructMember(1, CommandStatusIB, optional=True) class InvokeRequestMessage(tlv.Structure): diff --git a/circuitmatter/tlv.py b/circuitmatter/tlv.py index a7a7d8f..102ef0c 100644 --- a/circuitmatter/tlv.py +++ b/circuitmatter/tlv.py @@ -821,14 +821,13 @@ class AnythingMember(Member): """Stores a TLV encoded value.""" def decode(self, control_octet, buffer, offset=0): - print(f"anything 0x{control_octet:02x} buffer", buffer[offset:].hex(" ")) return None def print(self, value): if isinstance(value, bytes): return value.hex(" ") if isinstance(value, memoryview): - return "memoryview" + value.hex(" ") + return value.hex(" ") return str(value) def encode_element_type(self, value): diff --git a/test_data/recorded_packets.jsonl b/test_data/recorded_packets.jsonl index 4d49247..f71cdde 100644 --- a/test_data/recorded_packets.jsonl +++ b/test_data/recorded_packets.jsonl @@ -1,19 +1,27 @@ -["urandom", 615386218116488, 8, "H7JptUR2dI8="] -["receive", 615390509290148, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "BAAAAMsxPAn5lKlXrK6EIgUgHF8AABUwASBDN5VT4qEZHWLnSF7HoM8OgCtvfKkSpg26+++4bDQJmyUChtEkAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] -["urandom", 615390539734900, 32, "PaQiCjhxs8ODMZ5GthBmb4/MZ4hJigr5XQgxtBNfujM="] -["send", 615390539901344, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AQAAAJMHSAP5lKlXrK6EIgIhHF8AAMsxPAkVMAEgQzeVU+KhGR1i50hex6DPDoArb3ypEqYNuvvvuGw0CZswAiA9pCIKOHGzw4Mxnka2EGZvj8xniEmKCvldCDG0E1+6MyUDAQA1BCYBECcAADACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] -["receive", 615390546155785, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "BAAAAMwxPAn5lKlXrK6EIgUiHF8AABUwAUEE0gefLFj+xRmFkURaSBa2o9k+BK7IeSGuMUJd8vOlfzqxfeZWVNwPLq67Kc0dH8WfMo9nOJh1ViqUgtFGLY6pdxg="] -["randbelow", 615390546249471, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 74944978393438142966461922107606904469387315672506885538396514800631953724932] -["send", 615390561660608, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AQAAAJQHSAP5lKlXrK6EIgIjHF8AAMwxPAkVMAFBBNvQdssgf55XSmuJ7O6c4bd7LQNjaaf8kjD/yUCNkfo2UnP5+/klM4hRqOvJz3DE3HacfTrhYOtFwevfxuagAYEwAiAZMTjp8AuJYGksjRhRFtmQx9QSClKzPml4KfOTG5spvhg="] -["receive", 615390562074710, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "BAAAAM0xPAn5lKlXrK6EIgUkHF8AABUwASBfqp677T23h0QftAJ7cvU7+c6kBjML4AYBz1RL0aEp6Bg="] -["send", 615390562156564, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AQAAAJUHSAP5lKlXrK6EIgJAHF8AAM0xPAkAAAAAAAAAAA=="] -["receive", 615390562331504, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAADkFHQDX3mortC46vkGGtXfugQfkllyodwMhALyTz4s3J3MIlTvLuIYEWxIuGWQ4vKdcFv20DQxg5YBfykBE+Pz8I2i1YXlNa32KSQpNbcNI/Adz6EF1Y65N+siene6yPM+D0j9kNNGgqc5+kUwbBPhueoo3QPCT0QTOX2VnngU="] -["send", 615390563235680, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AIbRAJQzkwZaqHWX2x5Iu3noT87Cit/znpsXK5LT5bUk4dyDQX3u5X8S5TQZJucEx/Jx8lMMqu2q6Ws1cIYo1BruealZSIqoI5s3bnWKzQ2p5PDy6adWghpI6dwrSoCjIr9WmOg7PmlSt/ZF+92qHRyTJk6CILdCfdJ7eG4fzKQ8ed9akhCk7yoCyyO0sy6G4Xw/zVraVT+0saRtZ+d5h+Hv/Mxlm+YQO6k3ZCQZKsSFjSD38W4Qw+FMDTu5SVt3A8/FihppOa1/NzkRQ+zW54Egh6MmG0FWWQaCCkbdtNnz4CCsWmUDniAabCE+Py5Sd0UzRYiB6IRbwUJ1zUbQq1T71Zgos53G2eEuSru2wArQikThd7vTYwnrQ0qLWn5pa4guND4onTrB1mBIiRYJDFOD6uHZ9OXZ"] -["receive", 615390572146082, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAADoFHQDtsggGX7t7Yodo7vzgHLwu4KNA9k4+MR+q+GJmIHQ="] -["receive", 615390572246762, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAADsFHQCGLf05sAAi+NRh8y1W1AXSfBBqUdFFUaJ07gQq6uV3JR1ssKHk2qntj0vWjz8OaB2d5vla3bYvADTP0aEN8n2sLRe48oyzaAaeMEXqMg=="] -["send", 615390572532622, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AIbRAJUzkwbUOdyJspVGWQhX1NpL6PKsZIZ5ajCCrPJGg1N5pU+NA7PDlsuenOcn2JWdkwdOYx7WMN9A7Z9dfgJd8ooWGCU="] -["receive", 615390573914860, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAADwFHQA/IQ7uy5OJdjXnYZFbuuknc/bJn09dppQvhpw8fa4="] -["receive", 615390574036349, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAAD0FHQB4bFd583wEfXys5WQGiyo55a4qdiuvPofqp5CIKlqSAWhp1v1qy2vNpz3UK5Vmx0x5fNkSg4Fse8s="] -["send", 615390574416837, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AIbRAJYzkwaHh6H2vW2FUZ7DwMed6ErviWwSifdImMsGnpnbvebtNxKHOGo+fGZJdt9EcnBG+pJivWWKY3taEzXBD1rfr6vIklLsKkwVhXt8+o6BbzNTslC2PlGuR5UrTThld9K+J1urqw=="] -["receive", 615390576223385, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AAEAAD4FHQDL8Hq5PN6fTUZ4SToLwk55moIVzbDKdBh6+fj8pn0toERxbVSDI+i7bNqjxaJ+7RUndx9frvOsaso="] -["send", 615390576563778, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 48793, 0, 0], "AIbRAJczkwbClUzCYE3z9GkY1jbnKcRvzgkgM5Bwp3JXhTts8HdgDnn8pe2IrDO8zABfkuQ5G4VrRidWvQbtureE6+nxzSGIPSyup8AeuwrP8PhAKib/JcWj0M78TUSiJxq2QHfmZo8iwQ=="] +["urandom", 683495367840819, 8, "TVCaLwZCw7U="] +["receive", 683498821765516, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAF8dUQHg9sDgbF2ERwUgjxgAABUwASASKWbFjMO2TX2TttCy5g/kYva9Xkq3bUgRNk2ZJLC+MCUCVLskAwAoBDUFJQH0ASUCLAElA6APJAQRJAULJgYAAAMBJAcBGBg="] +["urandom", 683498851412134, 32, "MXZ+qUCRDQzGtLd/BdEFclIVyeARZatikL292J53wec="] +["send", 683498851505099, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ0RXQ7g9sDgbF2ERwIhjxgAAF8dUQEVMAEgEilmxYzDtk19k7bQsuYP5GL2vV5Kt21IETZNmSSwvjAwAiAxdn6pQJENDMa0t38F0QVyUhXJ4BFlq2KQvb3YnnfB5yUDAQA1BCYBECcAADACIObgj9CEx2MyPagRHuoX1OB32N8u1aKUpNKjb4b854YkGBg="] +["receive", 683498857474653, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAGAdUQHg9sDgbF2ERwUijxgAABUwAUEE20SFW+IKRC2BOynyHg8nHPTv3+LVnM+u4ETTCvbrvnTgkJQNNj/qtVQNZWLUPqJlGYTqUqMqQtXrIkE5M4Ih6Bg="] +["randbelow", 683498857567458, 115792089210356248762697446949407573529996955224135760342422259061068512044369, 55163913805809046006520747928954323885679067894514874675221401915371371566658] +["send", 683498872841814, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ4RXQ7g9sDgbF2ERwIjjxgAAGAdUQEVMAFBBAbpQBc797XYmoP1v/VvwWAkH1y/0bDLthCBm0x0+NeykQt9xIvt80/Bs/ho3rZp+4j+S19V7CvxDJBpB47VKPowAiBwLzDgwDToYO6xBjEMzXZ7MVUWXPf90Xa9lWnufbGeaxg="] +["receive", 683498873254503, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "BAAAAGEdUQHg9sDgbF2ERwUkjxgAABUwASB8b9Xw7GZRd3GHmsZJ3Nv29JDKXZbinVKGP2m8Qt61Xxg="] +["send", 683498873329966, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AQAAAJ8RXQ7g9sDgbF2ERwJAjxgAAGEdUQEAAAAAAAAAAA=="] +["receive", 683498873509204, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADB0XwokT96ZD3xGZKllpiUod7YhF1weUtaXchVJGbNRBI7DUPwBeY2uJgsA7Bor4qb+XsCPzxzdenS8hxQax5z7FCBOY6a5SSQi4mryj30rTt0SjrN6KdIknnTYQks8GXeMaz7VlOrdF2CplNRXF+AGxS1CXxiqr/kuhAw4WFE="] +["send", 683498874159352, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AME8eQ7ekgBSkCMyEut7Gwn6MYQenzBIWdNgAio6brAJTddb2DqSjWPUS1NQ2Nh6wwipFd1KRLH/F3ruQ5/+dSXcoPlV4J8T0Jm8eqx0o6DayguwFAyu1N5yiZk5bl5GmxYlEFlUi0VPOMcPPzYnqNbsYIgOAO63XcfBHNJsA5vCNsaKnqAktWHtzXmXtZTvZgEHJGJD8LIhZ6lM8wd1Ui3zFlB4GCG7dWjgn13z+TSbgHHHyt1dnj7a/uNxMwrWrQgSnkL/tzrBL0xSwAiVuwtBZpGdab85icqUkQeL6OZdYk21Oz9MVjHuGPrXq3npNX76/wgP3+rAeHc+r1Pt/qEd+KHbG4G+OHUTlIHygGth57dUDM5rum5AdW3/8YTx+tAncedBybrAlikzDh+SYpztfAQG"] +["receive", 683498875161114, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADF0XwrZIgz9knLzOrJkDSf9kwXRYOEpfY5UfzmZM7/qdko="] +["receive", 683498875255833, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADJ0Xwr/eCKktmaP1Uc1+Zp5aNJM9lEmVebne+lDI+DNiP2XImSTH8ARBUcbF9HLDK1/4dT55AGOts4iZ0ZNOZFVXAru45pVapgT8bN8uxPYgQ=="] +["send", 683498875486027, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMI8eQ4P2pPGapfOg39gOtTZgx8gbquT4vchxWodZJvZeTFpbBUvommihSJ/3+D7sTifa9gz+fM9rdyWF1rd4Cx7EyM="] +["receive", 683498875778780, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADN0XwoUf68e6LmehobmSAeEE7+bJ9hopGfmhjlTakDo8ug="] +["receive", 683498875892114, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADR0XwrxT37Olu9HyEQPuSGLVT3EjolLh8dB3hIb4YO/Q/vxakfp1Qs7D8fPhLUjI1jjph5HfD2ogeuQe1M="] +["send", 683498876146515, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMM8eQ7tgujEMBfFaIlrN/xCl8Y4a2JH5UqEYxuRQeJHjMpRBm6WTBFnXXA5KuInASn40rPnidJjVMgnXJvaUeNbqwsM2tIu"] +["receive", 683498876473603, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADV0XwqZojBtHTLIdbFe56V1UIE4WaooRa60ax+r6P31Jhxj1n075g/a3tNUOEfyBkmQJSgUH0MQxSz84D1YFtaZAw=="] +["send", 683498876708616, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMQ8eQ64jIF0YadOHTE9nqad5MkK1by0uhBSadiviVXlc03a1WLgBkySoX6214T1XIwpMTxWrZfVaV17z+S3jL2okeSrmdDE"] +["receive", 683498877025054, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADZ0Xwo9WH3ur9aGpocl4AKUtZkAFiAK87+d4cQqMjAHsUt5kTWAtvekb1R0w+/kYsC5QwVklDk4lvQ="] +["send", 683498877240551, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMU8eQ5vFsc6CWDDdoCuIls1wkgctrxuFrnBzZRCtLNK9Qt3HRb1C6vSRNJ0pGU27NuN6l2k9mEHCy0WNhERx8oxUP39"] +["receive", 683498877583799, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADd0XwrDKFXAh2biGtXh+eG+a37SSK7iHHclvobcxFZJT3TFpKlMw5y75O8h1yrySPNt+RKjWKqx13c="] +["send", 683498877788546, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMY8eQ6Is+Atiw2ABb7FL5iy1L6mM5g/k7414s1u2n/lLreAXNCJWDu+dhXcgeK8jXO+nGMOa+PDR8lpj4Lwrxi0MP/h"] +["receive", 683498878095526, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADh0Xwq1tLR8JSXMAh/4eBGVh9nvr6c7D5xraOqx3CTV4Usdv049V7JfXqJVAppEJvHMOXk+ZCUyObv60Q2+lgvaj+lGhA6W/b+5ztZVo8OJIcZ9Rll1FgrwTA=="] +["send", 683498878325730, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMc8eQ4DBvpgfxkI8u77TH5UJBeFjkKJgW4W1SxT6Gfp2ZKvNNJOjEDmt/G/vWgzZlvcUq9jKCqQdTkn2MlT0aSwItbPLjJ1"] +["receive", 683498878665963, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AAEAADl0Xwoh7zyfoLawEVHK/jEfWmf+jEPDbe5cme3DeR/UBcQz1OhT4f8oHNQk6nPIt+Ss0mNHWS7Fapss/z0="] +["send", 683498878871692, ["fd98:bbab:bd61:8040:642:1aff:fe0c:9f2a", 60184, 0, 0], "AFS7AMg8eQ5q8qqBOCg+y9xQcPIrn3pfJVHbKBSkMMzzOtyYKIMD/ZK4O0TLX8EqzIdOtmdET05jajUN5yWSG6rWZqQcaKiygGUc"]