# Lumino Settlement ### Contenido * ¿Qué es el proceso de settlement? * ¿Cómo funciona? * Code walkthrough * Conclusiones * Propuesta de implementacion de Settlement para LC ## ¿Qué es el proceso de settlement? Cuando abrimos un canal entre dos nodos el mismo nos sirve para realizar operaciones de pagos y depósitos. Las operaciones de pago son offchain, por lo que los saldos de cada extremo del canal mientras el canal esta abierto no estan actualizados onchain. Una vez se cierra un canal viene el proceso de settlement, dicho proceso corre luego de cierta cantidad de bloques de confirmación una vez el canal haya sido cerrado. El mismo permite contabilizar los balances (pagos realizados) en ambos extremos del canal para luego plasmarlos en la chain en una operacion on chain. ## ¿Cómo funciona? Basicamente luego de que el canal se confirmó como cerrado, Lumino espera un número determinado de bloques de confirmacion antes de ejecutar el proceso de settlement. El número de bloques es parametrizable y por defecto son 500 bloques en la configuracion actual. Dicho parámetro se especifica en el nodo pero a su vez es un parámetro del contrato que tiene un rango de bloques válido. Este rango es chequeado en el nodo antes de especificarlo en la llamada al contrato. Actualmente el parámetro del contrato tiene un mínimo y un máximo de bloques de confirmación de 500 bloques, lo cual hace que el parámetro no pueda modificarse en el nodo. El proceso lo que hace es contabilizar todos los pagos que se hayan realizado en el canal para confirmar el balance final y así poder crear las pruebas de balance. Es algo así como un cierre contable donde se confirman todos los balances y se plasma en un libro, en este caso el proceso confirma los balances en la chain. ## Code walkthrough Cuando un canal se cerró como se menciona anteriormente, se espera cierta cantidad de bloques de confirmación antes de ejecutar el proceso de settlement. La cantidad de bloques de confirmación esta dada por el contrato y el nodo encarga de chequear ese parámetro en cada iteración, aquí podemos ver como el nodo obtiene dicho valor dentro del archivo **raiden/network/proxies/payment_channel.py:85:** ```python def settle_timeout(self) -> int: """ Returns the channels settle_timeout. """ # There is no way to get the settle timeout after the channel has been closed as # we're saving gas. Therefore get the ChannelOpened event and get the timeout there. filter_args = get_filter_args_for_specific_event_from_channel( token_network_address=self.token_network.address, channel_identifier=self.channel_identifier, event_name=ChannelEvent.OPENED, contract_manager=self.contract_manager, ) events = self.token_network.proxy.contract.web3.eth.getLogs(filter_args) assert len(events) > 0, "No matching ChannelOpen event found." # we want the latest event here, there might have been multiple channels event = decode_event( self.contract_manager.get_contract_abi(CONTRACT_TOKEN_NETWORK), events[-1] ) return event["args"]["settle_timeout"] ``` El nodo tiene un manejo de estado que cambia a medida que los bloques en la chain se van minando. Dicho manejo se puede apreciar en el siguiente codigo dentro del archivo **raiden/transfer/channel.py:2064**: ```python def state_transition( channel_state: NettingChannelState, state_change: StateChange, block_number: BlockNumber, block_hash: BlockHash, ) -> TransitionResult[NettingChannelState]: # pylint: disable=too-many-branches,unidiomatic-typecheck events: List[Event] = list() iteration: TransitionResult[NettingChannelState] = TransitionResult(channel_state, events) if type(state_change) == Block: assert isinstance(state_change, Block), MYPY_ANNOTATION iteration = handle_block(channel_state, state_change, block_number) elif type(state_change) == ActionChannelClose: assert isinstance(state_change, ActionChannelClose), MYPY_ANNOTATION iteration = handle_action_close( channel_state=channel_state, close=state_change, block_number=block_number, block_hash=block_hash, ) elif type(state_change) == ActionChannelSetFee: assert isinstance(state_change, ActionChannelSetFee), MYPY_ANNOTATION iteration = handle_action_set_fee(channel_state=channel_state, set_fee=state_change) elif type(state_change) == ContractReceiveChannelClosed: assert isinstance(state_change, ContractReceiveChannelClosed), MYPY_ANNOTATION iteration = handle_channel_closed(channel_state, state_change) elif type(state_change) == ContractReceiveChannelClosedLight: assert isinstance(state_change, ContractReceiveChannelClosedLight), MYPY_ANNOTATION iteration = handle_channel_closed_light(channel_state, state_change) elif type(state_change) == ContractReceiveUpdateTransfer: assert isinstance(state_change, ContractReceiveUpdateTransfer), MYPY_ANNOTATION iteration = handle_channel_updated_transfer(channel_state, state_change, block_number) elif type(state_change) == ContractReceiveChannelSettled: assert isinstance(state_change, ContractReceiveChannelSettled), MYPY_ANNOTATION iteration = handle_channel_settled(channel_state, state_change) elif type(state_change) == ContractReceiveChannelSettledLight: assert isinstance(state_change, ContractReceiveChannelSettledLight), MYPY_ANNOTATION iteration = handle_channel_settled_light(channel_state, state_change) elif type(state_change) == ContractReceiveChannelNewBalance: assert isinstance(state_change, ContractReceiveChannelNewBalance), MYPY_ANNOTATION iteration = handle_channel_newbalance(channel_state, state_change, block_number) elif type(state_change) == ContractReceiveChannelBatchUnlock: assert isinstance(state_change, ContractReceiveChannelBatchUnlock), MYPY_ANNOTATION iteration = handle_channel_batch_unlock(channel_state, state_change) return iteration ``` En esta función se manejan multiples eventos, nosotros nos enfocaremos en el siguiente fragmento dentro de la función: if type(state_change) == Block: assert isinstance(state_change, Block), MYPY_ANNOTATION iteration = handle_block(channel_state, state_change, block_number) más especificamente en la llamada a `handle_block` que es una función que maneja el cambio de estado cuando es un nuevo bloque. Dicha función tiene el siguiente cuerpo en el mismo archivo en la linea 1806: ```python def handle_block( channel_state: NettingChannelState, state_change: Block, block_number: BlockNumber ) -> TransitionResult[NettingChannelState]: assert state_change.block_number == block_number events: List[Event] = list() if get_status(channel_state) == CHANNEL_STATE_CLOSED: msg = "channel get_status is STATE_CLOSED, but close_transaction is not set" assert channel_state.close_transaction, msg msg = "channel get_status is STATE_CLOSED, but close_transaction block number is missing" assert channel_state.close_transaction.finished_block_number, msg closed_block_number = channel_state.close_transaction.finished_block_number settlement_end = closed_block_number + channel_state.settle_timeout if state_change.block_number > settlement_end: channel_state.settle_transaction = TransactionExecutionStatus( state_change.block_number, None, None ) event = ContractSendChannelSettle( canonical_identifier=channel_state.canonical_identifier, triggered_by_block_hash=state_change.block_hash, channel_state= channel_state ) events.append(event) while is_deposit_confirmed(channel_state, block_number): order_deposit_transaction = heapq.heappop(channel_state.deposit_transaction_queue) apply_channel_newbalance(channel_state, order_deposit_transaction.transaction) return TransitionResult(channel_state, events) ``` donde se chequea el estado del canal, y como en este caso hablamos de un canal cerrado nos enfocaremos en el siguiente fragmento: ```python closed_block_number = channel_state.close_transaction.finished_block_number settlement_end = closed_block_number + channel_state.settle_timeout if state_change.block_number > settlement_end: channel_state.settle_transaction = TransactionExecutionStatus( state_change.block_number, None, None ) event = ContractSendChannelSettle( canonical_identifier=channel_state.canonical_identifier, triggered_by_block_hash=state_change.block_hash, channel_state= channel_state ) events.append(event) ``` aquí el número de bloque actual se compara con el bloque que fue calculado como el bloque final de confirmación. Si actualmente estamos mas allá de ese bloque **(bloque desde el que se cerro el canal + cantidad de bloques que se especifique en el parámetro)** entonces se crea un nuevo evento para ser procesado por la máquina de estados que disparará el proceso de settlement. Entonces en el caso de que estemos en condiciones de hacer un settlement el evento ```ContractSendChannelSettle``` es creado para ser manejado por la maquina de estados en la siguiente iteración. Si buscamos dicho evento en el manejador de eventos, podemos encontrarlo en el archivo **raiden/raiden_event_handler.py:169**: ```python elif type(event) == ContractSendChannelSettle: assert isinstance(event, ContractSendChannelSettle), MYPY_ANNOTATION self.handle_contract_send_channelsettle(raiden, event) ``` donde basicamente se ve como el evento es manejado por la función `handle_contract_send_channelsettle` dentro del mismo archivo bajo la linea 601: ```python @staticmethod def handle_contract_send_channelsettle( raiden: "RaidenService", channel_settle_event: ContractSendChannelSettle ): # only make settlements if our node is a participant, dont do it for lc if channel_settle_event.channel_state.our_state.address == raiden.address or channel_settle_event.channel_state.partner_state.address == raiden.address: assert raiden.wal, "The Raiden Service must be initialize to handle events" canonical_identifier = CanonicalIdentifier( chain_identifier=raiden.chain.network_id, token_network_address=channel_settle_event.token_network_identifier, channel_identifier=channel_settle_event.channel_identifier, ) triggered_by_block_hash = channel_settle_event.triggered_by_block_hash payment_channel: PaymentChannel = raiden.chain.payment_channel( canonical_identifier=canonical_identifier ) token_network_proxy: TokenNetwork = payment_channel.token_network if not token_network_proxy.client.can_query_state_for_block(triggered_by_block_hash): # The only time this can happen is during restarts after a long time # when the triggered block ends up getting pruned # In that case it's safe to just use the latest view of the chain to # query the on-chain participant/channel details triggered_by_block_hash = token_network_proxy.client.blockhash_from_blocknumber( "latest" ) participants_details = token_network_proxy.detail_participants( participant1=payment_channel.participant1, participant2=payment_channel.participant2, block_identifier=triggered_by_block_hash, channel_identifier=channel_settle_event.channel_identifier, ) our_details = participants_details.our_details partner_details = participants_details.partner_details log_details = { "chain_id": canonical_identifier.chain_identifier, "token_network_identifier": canonical_identifier.token_network_address, "channel_identifier": canonical_identifier.channel_identifier, "node": pex(raiden.address), "partner": to_checksum_address(partner_details.address), "our_deposit": our_details.deposit, "our_withdrawn": our_details.withdrawn, "our_is_closer": our_details.is_closer, "our_balance_hash": to_hex(our_details.balance_hash), "our_nonce": our_details.nonce, "our_locksroot": to_hex(our_details.locksroot), "our_locked_amount": our_details.locked_amount, "partner_deposit": partner_details.deposit, "partner_withdrawn": partner_details.withdrawn, "partner_is_closer": partner_details.is_closer, "partner_balance_hash": to_hex(partner_details.balance_hash), "partner_nonce": partner_details.nonce, "partner_locksroot": to_hex(partner_details.locksroot), "partner_locked_amount": partner_details.locked_amount, } if our_details.balance_hash != EMPTY_HASH: event_record = get_event_with_balance_proof_by_balance_hash( storage=raiden.wal.storage, canonical_identifier=canonical_identifier, balance_hash=our_details.balance_hash, ) if event_record.data is None: log.critical("our balance proof not found", **log_details) raise RaidenUnrecoverableError( "Our balance proof could not be found in the database" ) our_balance_proof = event_record.data.balance_proof our_transferred_amount = our_balance_proof.transferred_amount our_locked_amount = our_balance_proof.locked_amount our_locksroot = our_balance_proof.locksroot else: our_transferred_amount = 0 our_locked_amount = 0 our_locksroot = EMPTY_HASH if partner_details.balance_hash != EMPTY_HASH: state_change_record = get_state_change_with_balance_proof_by_balance_hash( storage=raiden.wal.storage, canonical_identifier=canonical_identifier, balance_hash=partner_details.balance_hash, sender=participants_details.partner_details.address, ) if state_change_record.data is None: log.critical("partner balance proof not found", **log_details) raise RaidenUnrecoverableError( "Partner balance proof could not be found in the database" ) partner_balance_proof = state_change_record.data.balance_proof partner_transferred_amount = partner_balance_proof.transferred_amount partner_locked_amount = partner_balance_proof.locked_amount partner_locksroot = partner_balance_proof.locksroot else: partner_transferred_amount = 0 partner_locked_amount = 0 partner_locksroot = EMPTY_HASH payment_channel.settle( transferred_amount=our_transferred_amount, locked_amount=our_locked_amount, locksroot=our_locksroot, partner_transferred_amount=partner_transferred_amount, partner_locked_amount=partner_locked_amount, partner_locksroot=partner_locksroot, block_identifier=triggered_by_block_hash, ) else: log.info("Ignoring settlement cause is a light client") ``` Si prestamos atención a esta función podemos ver que se obtienen las pruebas de balance asi como los detalles de los saldos para poder utilizar dichos datos en el settlement que se realiza en el fragmento final. Para los LC el settlement esta siendo ignorado actualmente ya que no hay una implementacion disponible aun, es por eso que vemos el statement else con un simple log. Aquí está el fragmento final donde se puede apreciar la llamada a settle: ```python payment_channel.settle( transferred_amount=our_transferred_amount, locked_amount=our_locked_amount, locksroot=our_locksroot, partner_transferred_amount=partner_transferred_amount, partner_locked_amount=partner_locked_amount, partner_locksroot=partner_locksroot, block_identifier=triggered_by_block_hash, ) ``` done la llamada a la funcion settle está dentro de `PaymentChannel` que se ubica en el archivo **raiden/network/proxies/payment_channel.py:284**, a su vez la función definida allí es tan solo un proxy de la funcion settle dentro de `TokenNetwork`, por lo que mostraremos la función final ubicada en el archivo **raiden/network/proxies/token_network.py:2149** ```python def settle( self, channel_identifier: ChannelID, transferred_amount: TokenAmount, locked_amount: TokenAmount, locksroot: Locksroot, partner: Address, partner_transferred_amount: TokenAmount, partner_locked_amount: TokenAmount, partner_locksroot: Locksroot, given_block_identifier: BlockSpecification, ): """ Settle the channel. """ log_details = { "channel_identifier": channel_identifier, "token_network": pex(self.address), "node": pex(self.node_address), "partner": pex(partner), "transferred_amount": transferred_amount, "locked_amount": locked_amount, "locksroot": encode_hex(locksroot), "partner_transferred_amount": partner_transferred_amount, "partner_locked_amount": partner_locked_amount, "partner_locksroot": encode_hex(partner_locksroot), } log.debug("settle called", **log_details) checking_block = self.client.get_checking_block() # and now find out our_maximum = transferred_amount + locked_amount partner_maximum = partner_transferred_amount + partner_locked_amount # The second participant transferred + locked amount must be higher our_bp_is_larger = our_maximum > partner_maximum if our_bp_is_larger: kwargs = { "participant1": partner, "participant1_transferred_amount": partner_transferred_amount, "participant1_locked_amount": partner_locked_amount, "participant1_locksroot": partner_locksroot, "participant2": self.node_address, "participant2_transferred_amount": transferred_amount, "participant2_locked_amount": locked_amount, "participant2_locksroot": locksroot, } else: kwargs = { "participant1": self.node_address, "participant1_transferred_amount": transferred_amount, "participant1_locked_amount": locked_amount, "participant1_locksroot": locksroot, "participant2": partner, "participant2_transferred_amount": partner_transferred_amount, "participant2_locked_amount": partner_locked_amount, "participant2_locksroot": partner_locksroot, } try: self._settle_preconditions( channel_identifier=channel_identifier, partner=partner, block_identifier=given_block_identifier, ) except NoStateForBlockIdentifier: # If preconditions end up being on pruned state skip them. Estimate # gas will stop us from sending a transaction that will fail pass with self.channel_operations_lock[partner]: error_prefix = "Call to settle will fail" gas_limit = self.proxy.estimate_gas( checking_block, "settleChannel", channel_identifier=channel_identifier, **kwargs ) if gas_limit: error_prefix = "settle call failed" gas_limit = safe_gas_limit(gas_limit, GAS_REQUIRED_FOR_SETTLE_CHANNEL) transaction_hash = self.proxy.transact( "settleChannel", gas_limit, channel_identifier=channel_identifier, **kwargs ) self.client.poll(transaction_hash) receipt_or_none = check_transaction_threw(self.client, transaction_hash) transaction_executed = gas_limit is not None if not transaction_executed or receipt_or_none: if transaction_executed: block = receipt_or_none["blockNumber"] else: block = checking_block self.proxy.jsonrpc_client.check_for_insufficient_eth( transaction_name="settleChannel", address=self.node_address, transaction_executed=transaction_executed, required_gas=GAS_REQUIRED_FOR_SETTLE_CHANNEL, block_identifier=block, ) msg = self._check_channel_state_after_settle( participant1=self.node_address, participant2=partner, block_identifier=block, channel_identifier=channel_identifier, ) error_msg = f"{error_prefix}. {msg}" log.critical(error_msg, **log_details) raise RaidenUnrecoverableError(error_msg) log.info("settle successful", **log_details) ``` Finalmente allí vemos como se prepara y se envía la transacción de `settleChannel` que es la encargada de fijar las pruebas de balance en la chain. A continuacion vamos a analizar los fragmentos principales de la misma para entender un poco mejor el funcionamiento. Primero vemos como se hace un chequeo de los datos necesarios para realizar la transacción, los datos son: **address, transferred amount, locked amount y locksroot**. Estos datos son requeridos para cada participant, es decir para cada extremo del canal, por lo que es necesario obtener los datos del partner en el otro extremo. Esto es lo que vemos en el primer fragmento de la funcion: ```python checking_block = self.client.get_checking_block() # and now find out our_maximum = transferred_amount + locked_amount partner_maximum = partner_transferred_amount + partner_locked_amount # The second participant transferred + locked amount must be higher our_bp_is_larger = our_maximum > partner_maximum if our_bp_is_larger: kwargs = { "participant1": partner, "participant1_transferred_amount": partner_transferred_amount, "participant1_locked_amount": partner_locked_amount, "participant1_locksroot": partner_locksroot, "participant2": self.node_address, "participant2_transferred_amount": transferred_amount, "participant2_locked_amount": locked_amount, "participant2_locksroot": locksroot, } else: kwargs = { "participant1": self.node_address, "participant1_transferred_amount": transferred_amount, "participant1_locked_amount": locked_amount, "participant1_locksroot": locksroot, "participant2": partner, "participant2_transferred_amount": partner_transferred_amount, "participant2_locked_amount": partner_locked_amount, "participant2_locksroot": partner_locksroot, } ``` También vemos como se obtiene el amount máximo para chequear el orden en el que serán especificados los participants, esto es algo necesario en la transacción por eso tienen que ir en orden, el participant 2 tiene que ser siempre el que mayor amount tenga. A continuacion veremos como se chequean las precondiciones para evitar realizar una transaccion inválida y gastar gas sin sentido: ```python try: self._settle_preconditions( channel_identifier=channel_identifier, partner=partner, block_identifier=given_block_identifier, ) except NoStateForBlockIdentifier: # If preconditions end up being on pruned state skip them. Estimate # gas will stop us from sending a transaction that will fail pass ``` **NOTA: Evaluando este segmento de código, parece ser que es algo que podría hacerse mejor o directamente no hacerse. El chequeo de la precondicion está bien y en el caso de que falle se levanta una excepción. Luego es donde viene el problema, en el catch la excepción levantada anteriormente se ignora sin ningun manejo especial, lo cual es lo mismo que si no estuvieramos chequeando ninguna precondicion. Suponemos que es porque la función de chequeo tiene un código comentado (deberíamos chequear esto probablemente)** Si continuamos con el análisis podemos ver que en el siguiente segmento de codigo se encuentra el envío de la transaccion a la chain: ```python with self.channel_operations_lock[partner]: error_prefix = "Call to settle will fail" gas_limit = self.proxy.estimate_gas( checking_block, "settleChannel", channel_identifier=channel_identifier, **kwargs ) if gas_limit: error_prefix = "settle call failed" gas_limit = safe_gas_limit(gas_limit, GAS_REQUIRED_FOR_SETTLE_CHANNEL) transaction_hash = self.proxy.transact( "settleChannel", gas_limit, channel_identifier=channel_identifier, **kwargs ) self.client.poll(transaction_hash) receipt_or_none = check_transaction_threw(self.client, transaction_hash) ``` donde vemos como se bloquea la ejecución para realizar la operación, se chequea el gas limit y si todo está en orden se ejecuta la transacción obteniendo el hash y receipt resultantes. Finalmente vemos como la función chequea el resultado de la transacción: ```python transaction_executed = gas_limit is not None if not transaction_executed or receipt_or_none: if transaction_executed: block = receipt_or_none["blockNumber"] else: block = checking_block self.proxy.jsonrpc_client.check_for_insufficient_eth( transaction_name="settleChannel", address=self.node_address, transaction_executed=transaction_executed, required_gas=GAS_REQUIRED_FOR_SETTLE_CHANNEL, block_identifier=block, ) msg = self._check_channel_state_after_settle( participant1=self.node_address, participant2=partner, block_identifier=block, channel_identifier=channel_identifier, ) error_msg = f"{error_prefix}. {msg}" log.critical(error_msg, **log_details) raise RaidenUnrecoverableError(error_msg) log.info("settle successful", **log_details) ``` donde si existe algún problema se hacen unos chequeos para obtener la causa y luego se levanta una excepción con los detalles. En el caso de éxito simplemente se logea que el settlement fue exitoso terminando el proceso correctamente. Una vez se ejecuta el proceso correctamente, en la chain existe un evento que es escuchado por el nodo de lumino; ```ContractReceiveChannelSettled``` el mismo se manejado una vez se dispara para actualizar el estado del canal, esto lo podemos ver en el archivo **raiden/transfer/channel.py:2098**: ```python elif type(state_change) == ContractReceiveChannelSettled: assert isinstance(state_change, ContractReceiveChannelSettled), MYPY_ANNOTATION iteration = handle_channel_settled(channel_state, state_change) ``` donde ```handle_channel_settled``` ubicada en el mismo archivo bajo la linea 1949 es la encargada de resolver el manejo de dicho evento de la siguiente forma: ```python def handle_channel_settled( channel_state: NettingChannelState, state_change: ContractReceiveChannelSettled ) -> TransitionResult[NettingChannelState]: events: List[Event] = list() if state_change.channel_identifier == channel_state.identifier: set_settled(channel_state, state_change.block_number) our_locksroot = state_change.our_onchain_locksroot partner_locksroot = state_change.partner_onchain_locksroot should_clear_channel = ( our_locksroot == EMPTY_MERKLE_ROOT and partner_locksroot == EMPTY_MERKLE_ROOT ) if should_clear_channel: return TransitionResult(None, events) channel_state.our_state.onchain_locksroot = our_locksroot channel_state.partner_state.onchain_locksroot = partner_locksroot onchain_unlock = ContractSendChannelBatchUnlock( canonical_identifier=channel_state.canonical_identifier, participant=channel_state.partner_state.address, triggered_by_block_hash=state_change.block_hash, ) events.append(onchain_unlock) return TransitionResult(channel_state, events) ``` que como vemos lo que hace es simplemente ver si se debe limpiar el canal chequeando cada locksroot contra el **EMPTY_MERKLE_ROOT**. Si el canal no debe ser borrado se crea un evento nuevo llamado ```ContractSendChannelBatchUnlock``` que basicamente es un unlock on chain a manejar por otro event handler dentro de lumino (se puede encontrar mas detalle de ese proceso en este [link](https://hackmd.io/mO8suX8URYmhYtf2oRLPQA)). Si el canal es limpiado el proceso termina. ## Conclusiones Ahora que tenemos un conocimiento basico del funcionamiento de este proceso podemos responder algunas preguntas 1. **¿Quién se encarga de saber cuando debería mandar el settlement, otro polling?** Esto es manejado por la máquina de estados como vimos en el análisis, es decir que cuando pasaron más de un determinado número de bloques de confirmación y el canal esta en estado cerrado, se ejecuta la operación de settlement. Podríamos decir que es un polling pero en realidad es simplemente el manejo génerico de bloques de la máquina de estado de Lumino, controlando el evento de bloque y chequeando si el último bloque de confirmación ya paso y si el canal esta en estado cerrado. 2. **¿El LC chequea last block y si paso 500 lo ejecuta?** Si es LC el settlement es ignorado. 3. **¿Para el settlement se necesita información del otro lado del canal (ejemplo: el locksroot)?** Para el settlement se necesita información del otro lado tanto como del nuestro. Es decir, como se especificó en el análisis más arriba: **address, transferred amount, locked amount y locksroot**. 4. **¿Que situaciones de borde pueden haber?.** Que se haga un settlement en un canal sin pagos realizados * Esto se probo y no tiene repercusiones, el nodo maneja bien estos casos. Que se haga settlement cuando ya lo hizo el otro extremo, es decir puede ocurrir que se haya ejecutado el settlement en un extremo y como lo único que se chequea es el número de bloque de confirmación y el estado de canal, puede que el otro extremo ejecute la misma operación resultando en un error de concurrencia, que probablemente termine con alguno de los dos extremos fallando. Es un riesgo mínimo dado que al ejecutar el settlement estamos cambiando el estado del canal a **waiting_for_settle**, de todas formas existe la ventana de posibilidad. * Se comprobó que ambos extremos ejecutan el settle al mismo tiempo y que la primer operación que logre confirmarse cancela la del extremo opuesto, causando que en el extremo que no fue confirmada muestre un error en la consola del nodo como el siguiente: ```2020-08-20 22:44:21.145278 [error ] Channel is already unlocked. It cannot be settled [raiden.raiden_service]``` Esto indica que el caso esta manejado correctamente dado que se detecta el error y el canal se quita de la lista en ambos extremos. ## Propuesta de implementacion de Settlement para LC Una forma de implementar esto es manejar el evento en el statement `else` que no esta implementado en este momento **raiden/raiden_event_handler.py:601**: else: log.info("Ignoring settlement cause is a light client") Lo que se debería hacer allí es crear un mensaje para ser manejado por el LC, donde se debería poner que el settlement es requerido para que el LC pase los datos necesarios para la operación. Luego habría que crear un endpoint o usar alguno ya existente para ejecutar el settlement desde el LC. El manejo del settlement desde el LC debería usar la lógica que ya esta en el statement if de la funcion ubicada en **raiden/raiden_event_handler.py:601** modificando la misma solo para adaptar los parametros al LC. ## Implementación de Settlement Light Se implemento settlement light para manejar el proceso de settlement desde un light client cuando se recibe un evento de Settlement en un canal. ### Cómo funciona El nodo de lumino chequea en cada bloque el canal o los canales cerrados para ver si hay que ejecutar el settlement para alguno de ellos. Una vez pasa el bloque de confirmacion determinado, el nodo genera un evento que depende de si el canal es light o no, el evento que ejecuta. Nosotros nos enfocaremos en el caso de light channels, en dicho caso el evento generado es ContractSendChannelSettleLight, que es manejado por el nodo de tal forma que genera un mensaje para que sea manejado por el light client posteriormente en su long polling. El mensaje generado por el nodo tiene una estructura similar al siguiente ejemplo: ```json { "internal_msg_identifier": 29, "message_type": "LightClientProtocolMessageType.SettlementRequired", "message_content": { "payment_id": null, "message_order": 0, "message": { "channel_identifier": 37, "channel_network_identifier": "0x990326536fc4d4107758b527f23443e9e76484f0", "participant1": "0x590eb134cae8a9e6874803370ac499050951ad7e", "participant1_transferred_amount": 0, "participant1_locked_amount": 0, "participant1_locksroot": "0x0000000000000000000000000000000000000000000000000000000000000000", "participant2": "0x7eeb980779076dc3763cec72ecfd376616fa392c", "participant2_transferred_amount": 1000000000000000000, "participant2_locked_amount": 0, "participant2_locksroot": "0x0000000000000000000000000000000000000000000000000000000000000000" }, "is_signed": false } } ``` Donde si analizamos dicha estructura vemos que **payment_id** es null, **message_order** es 0 y **is_signed** es false, dichos parámetros para el caso de settlement no son requeridos por lo que pueden ser ignorados. Luego tenemos que **message_type** es del tipo ```LightClientProtocolMessageType.SettlementRequired``` lo cual indica que se requiere ejecutar un settlement. Si prestamos atención al field **message** allí encontraremos el cuerpo del mensaje que nos indica todos los parámetros requeridos para firmar la operación y enviarla a travez del nodo. A continuación detallamos los parámetros del cuerpo del mensaje: ```json { ... "message": { "channel_identifier": 37, "channel_network_identifier": "0x990326536fc4d4107758b527f23443e9e76484f0", "participant1": "0x590eb134cae8a9e6874803370ac499050951ad7e", "participant1_transferred_amount": 0, "participant1_locked_amount": 0, "participant1_locksroot": "0x0000000000000000000000000000000000000000000000000000000000000000", "participant2": "0x7eeb980779076dc3763cec72ecfd376616fa392c", "participant2_transferred_amount": 1000000000000000000, "participant2_locked_amount": 0, "participant2_locksroot": "0x0000000000000000000000000000000000000000000000000000000000000000" }, ... } ``` Básicamente tenemos los parámetros **channel_identifier** y **channel_network_identifier** que nos sirve para saber que canal y en que network tenemos que realizar el settlement. Luego tenemos el conjunto de parametros: **participant1**, **participant1_transferred_amount**, **participant1_locked_amount**, **participant1_locksroot**, **participant2**, **participant2_transferred_amount**, **participant2_locked_amount** y **participant2_locksroot** que nos sirven para crear la transaccion de settlement. Dicho conjunto de parámetros es necesario por el metodo **settleChannel** del contrato **TokenNetwork**. Estos parametros deben ser enviados en el orden en que vienen en el mensaje al contrato. Una vez creada y firmada la transacción, debe ser enviada al endpoint de patch_light_channel utilizado para depósitos y close actualmente ( ```/light_channels/<hexaddress:token_address>/<hexaddress:creator_address>/<hexaddress:partner_address>```). Ahora dicho endpoint tiene un parámetro nuevo y opcional que es utilizado para enviar la transacción firmada, dicho parámetro es **signed_settle_tx**. Además el parámetro **state** para este caso debe ser seteado con el valor ```waiting_for_settle```. Dicho endpoint chequea algunas precondiciones y envía la transaccion firmada a la blockchain esperando por la respuesta. Este endpoint responde lo mismo que en el close y deposit, el estado actualizado del canal. Puede fallar cuando el proceso de settlement ya fue ejecutado, por algun error de gas limit o falta de fondos para poder ejecutar la operación.