diff --git a/tests/mbt/model/ccv.qnt b/tests/mbt/model/ccv.qnt index cbd664b326..efe865ee4e 100644 --- a/tests/mbt/model/ccv.qnt +++ b/tests/mbt/model/ccv.qnt @@ -287,543 +287,118 @@ module ccv_types { ) } - -} - -module ccv { - // Implements the core logic of the cross-chain validation protocol. - - // Things that are not modelled: - // * Reward distribution - // * Slashes - - // Things that explicitly are modelled: - // * Validator set changes are propagated from provider to consumers - // * Vsc packets mature - - // We assume that packet receive + ack happen synchronously, - // i.e. when the packet is delivered, the ack is delivered right afterwards. - // This is because it is nontrivial in practice to get a relayer to relay a packet, but not its ack. - - import Time.* from "./libraries/Time" - import extraSpells.* from "./libraries/extraSpells" - import ccv_types.* - - // =================== - // PROTOCOL PARAMETERS + // UTILITY FUNCTIONS + // which do not hold the core logic of the protocol, but are still part of it // =================== - // the set of all possible consumer chains. - const ConsumerChains: Set[Chain] - - // For each chain, this defines the time between the initiation of an unbonding and its maturation. - const UnbondingPeriodPerChain: Chain -> int - - // The maximum time duration between sending any VscPacket to any consumer chain and receiving the - // corresponding VscMaturedPacket, without timing out the consumer chain and consequently removing it. - const VscTimeout: int - - // The timeoutTimestamp for sent packets. Can differ by chain. - const CcvTimeout: Chain -> int + // Appends the key assignment for the given oldConsAddr on the consumer by a validator + // to be pruned when a VscMaturedPacket for the current runningVscId is received from the consumer. + pure def AppendConsumerAddrToPrune(currentState: ProtocolState, oldConsAddr: ConsumerAddr, consumer: Chain): ProtocolState = { + pure val vscId = currentState.providerState.runningVscId + pure val consumerAddrsToPrune = currentState.providerState.consumerAddrsToPrune.getOrElse(consumer, Map()) + pure val prevConsAddrs = consumerAddrsToPrune.getOrElse(oldConsAddr, Map()).getOrElse(vscId, []) - // The trusting period on each chain. - // If headers on a channel between two chains are not updated within this period, - // they expire and the channel will be closed. - const TrustingPeriodPerChain: Chain -> int + pure val newConsAddrsToPrune = consumerAddrsToPrune.put(vscId, prevConsAddrs.append(oldConsAddr)) - // =================== - // PROTOCOL LOGIC contains the meat of the protocol - // functions here roughly correspond to API calls that can be triggered from external sources - // =================== - - // the power of a validator on the provider chain is changed by the given amount. We do not care how this happens, - // e.g. via undelegations, or delegations, ... - pure def votingPowerChange(currentState: ProtocolState, validator: Node, amount: int): Result = { - pure val currentValidatorSet = currentState.providerState.chainState.currentValidatorSet - pure val oldVotingPower = if (currentValidatorSet.keys().contains(validator)) currentValidatorSet.get(validator) else 0 - if (amount == 0) { - Err("Voting power cannot change by zero!") - } else if (oldVotingPower == 0) { - Err("Voting power cannot be changed for a validator that is not in the current validator set!") - } else { - pure val newVotingPower = oldVotingPower + amount - pure val newValidatorSet = currentValidatorSet.put(validator, if(newVotingPower >= 0) newVotingPower else 0) - // if the new validator set contains no validators with positive voting power, the validator set is invalid - if (newValidatorSet.values().filter(votingPower => votingPower > 0).size() == 0) { - Err("Voting power change resulted in empty validator set!") - } else { - // set the validator set changed flag - val newProviderState = currentState.providerState.with( - "providerValidatorSetChangedInThisBlock", true - ) - pure val tmpState = currentState.with( - "providerState", newProviderState - ) - pure val newState = setProviderValidatorSet(tmpState, newValidatorSet) - Ok(newState) - } - } + currentState.with( + "providerState", + currentState.providerState.with( + "consumerAddrsToPrune", + currentState.providerState.consumerAddrsToPrune.put(consumer, newConsAddrsToPrune) + ) + ) } - // Delivers the next queued VscMaturedPacket from a consumer chain to the provider chain. - // Arguments are the currentState and the the consumer chain, from which the packet will be delivered. - // If this packet will time out on the provider on delivery, - // the consumer will be dropped. - // The first return is the result of the operation, the second result is a boolean - // that indicates whether the consumer timed out or not. - // If the result has an error, the second return should be ignored. - pure def deliverPacketToProvider(currentState: ProtocolState, sender: Chain): (Result, bool) = { - if (not(isRunningConsumer(sender, currentState.providerState))) { - (Err("Sender is not currently a consumer - must have 'running' status!"), false) - } else if (length(currentState.consumerStates.get(sender).outstandingPacketsToProvider) == 0) { - (Err("No outstanding packets to deliver"), false) - } else { - val packet = currentState.consumerStates.get(sender).outstandingPacketsToProvider.head() - if(packet.timeoutTime <= currentState.providerState.chainState.runningTimestamp) { - // drop consumer - val result = stopConsumers( - currentState.providerState.consumerStatus, - Set(), - Set(sender) + // Returns the new ConsumerStatusMap according to the consumers to stop + // and the consumers to time out. + // If a consumer is both stopped and timed out, it will be timed out. + // The second return is an error string: If it is not equal to "", + // it contains an error message, and the first return should be ignored. + pure def stopConsumers( + currentConsumerStatusMap: Chain -> str, + consumersToStop: Set[Chain], + consumersToTimeout: Set[Chain]): (Chain -> str, str) = { + val runningConsumers = currentConsumerStatusMap.keys().filter( + chain => currentConsumerStatusMap.get(chain) == RUNNING + ) + // all consumers to stop must be running right now, else we have an error + if (consumersToStop.exclude(runningConsumers).size() > 0) { + (currentConsumerStatusMap, "Cannot stop a consumer that is not running") + } else { + val newConsumerStatusMap = currentConsumerStatusMap.keys().mapBy( + (chain) => + if (consumersToTimeout.contains(chain)) { + TIMEDOUT + } else if (consumersToStop.contains(chain)) { + STOPPED + } else { + currentConsumerStatusMap.get(chain) + } ) - - val newConsumerStatus = result._1 - val err = result._2 - if (err != "") { - (Err(err), false) - } else { - val newProviderState = currentState.providerState.with( - "consumerStatus", newConsumerStatus - ) - val newState: ProtocolState = currentState.with( - "providerState", newProviderState - ) - (Ok(newState), true) // true because the packet timed out - } + (newConsumerStatusMap, "") + } + } + + // Returns the new ConsumerStatusMap according to the consumers to start. + // The second return is an error string: If it is not equal to "", + // it contains an error message, and the first return should be ignored. + pure def startConsumers( + currentConsumerStatusMap: Chain -> str, + consumersToStart: Set[Chain]): (Chain -> str, str) = { + val nonConsumers = currentConsumerStatusMap.keys().filter( + chain => currentConsumerStatusMap.get(chain) == NOT_CONSUMER + ) + // all consumers to start must be nonConsumers right now, otherwise we have an error + if (consumersToStart.exclude(nonConsumers).size() > 0) { + (currentConsumerStatusMap, "cannot start a consumer that is stopped or already a consumer") } else { - // the packet has not timed out, so receive it on the provider - val result = recvPacketOnProvider(currentState, sender, packet) - val tmpState = result.newState - if (result.hasError()) { - (result, false) - } else { - (Ok(removeOutstandingPacketFromConsumer(tmpState, sender)), false) // false because the packet did not time out - } + val newConsumerStatusMap = currentConsumerStatusMap.keys().mapBy( + (chain) => + if (consumersToStart.contains(chain)) { + RUNNING + } else { + currentConsumerStatusMap.get(chain) + } + ) + (newConsumerStatusMap, "") } } - } - // Delivers the next queued VscPacket from the provider chain to a consumer chain. - // Arguments are the current state and the consumer chain, to which the packet will be delivered. - // If this packet will time out on the consumer on delivery, - // the consumer will be dropped. - // The first return is the result of the operation, the second result is a boolean - // that indicates whether the consumer timed out or not. - // If the result has an error, the second return should be ignored. - pure def deliverPacketToConsumer(currentState: ProtocolState, receiver: Chain): (Result, bool) = { - if (not(isRunningConsumer(receiver, currentState.providerState))) { - (Err("Receiver is not currently a consumer - must have 'running' status!"), false) - } else if (length(currentState.providerState.outstandingPacketsToConsumer.get(receiver)) == 0) { - (Err("No outstanding packets to deliver"), false) + pure def StartStopConsumers( + currentConsumerStatusMap: Chain -> str, + consumersToStart: Set[Chain], + consumersToStop: Set[Chain], + consumersToTimeout: Set[Chain] + ): (Chain -> str, str) = { + // check if any consumer is both started and stopped + if (consumersToStart.intersect(consumersToStop).size() > 0) { + (currentConsumerStatusMap, "Cannot start and stop a consumer at the same time") } else { - val packet = currentState.providerState.outstandingPacketsToConsumer.get(receiver).head() - // check if the consumer timed out - if (packet.timeoutTime <= currentState.consumerStates.get(receiver).chainState.runningTimestamp) { - // drop consumer - val result = stopConsumers( - currentState.providerState.consumerStatus, - Set(), - Set(receiver) - ) - - val newConsumerStatus = result._1 - val err = result._2 - if (err != "") { - (Err(err), false) - } else { - val newProviderState = currentState.providerState.with( - "consumerStatus", newConsumerStatus - ) - val newState: ProtocolState = currentState.with( - "providerState", newProviderState - ) - (Ok(newState), true) // true because the packet timed out - } + val res1 = currentConsumerStatusMap.startConsumers(consumersToStart) + val newConsumerStatus = res1._1 + val err1 = res1._2 + val res2 = newConsumerStatus.stopConsumers(consumersToStop, consumersToTimeout) + val err2 = res2._2 + if (err1 != "") { + (currentConsumerStatusMap, err1) + } else if (err2 != "") { + (currentConsumerStatusMap, err2) } else { - // the packet has not timed out, so receive it on the consumer - val result = recvPacketOnConsumer(currentState, receiver, packet) - val tmpState = result.newState - if (result.hasError()) { - (result, false) - } else { - (Ok(removeOutstandingPacketFromProvider(tmpState, receiver)), false) // false because the packet did not time out - } + (res2._1, "") } } } - - /// Ends a block on the provider. This means that the current validator set is committed on chain, - /// packets are queued, and the next block is started. Also, consumers that have passed - /// the VscTimeout without responding to a pending vscpacket are dropped. - pure def endAndBeginBlockForProvider( - currentState: ProtocolState, - // by how much the timestamp should be advanced, - // i.e. the timestamp for the next block is oldTimestamp + timeAdvancement - timeAdvancement: Time, - // a set of consumers that were not consumers before, but should be set to running now. - consumersToStart: Set[Chain], - // a set of consumers that were running before, but should be set to stopped now. - // This argument only needs to contain "voluntary" stops - - // forced stops, e.g. because a consumer timed out, - // will be added automatically. - consumersToStop: Set[Chain]): Result = { - val currentProviderState = currentState.providerState - - // check for vsc timeouts - val timedOutConsumers = getRunningConsumers(currentProviderState).filter( - consumer => - val res = TimeoutDueToVscTimeout(currentState, consumer) - res._1 - ) - - // for each consumer chain, apply the key assignment to the current validator set - val currentValSets = ConsumerChains.mapBy( - (consumer) => - currentProviderState.applyKeyAssignmentToValSet( - consumer, - currentProviderState.chainState.currentValidatorSet - ) - ) - // store the current validator set with the key assignments applied in the history - val newKeyAssignedValSetHistory = currentValSets.keys().mapBy( - (consumer) => - currentProviderState.keyAssignedValSetHistory - .getOrElse(consumer, List()) // get the existing history (empty list if no history yet) - .prepend(currentValSets.get(consumer)) // prepend the current validator set with key assignments applied - ) - - // run the shared core chainState logic - val newChainState = currentProviderState.chainState.endAndBeginBlockShared(timeAdvancement) - val providerStateAfterTimeAdvancement = - {...currentProviderState, chainState: newChainState, keyAssignedValSetHistory: newKeyAssignedValSetHistory} - val tmpState = currentState.with( - "providerState", providerStateAfterTimeAdvancement - ) - - - // send vsc packets (will be a noop if no sends are necessary) - val providerStateAfterSending = - providerStateAfterTimeAdvancement.sendVscPackets(currentProviderState.chainState.runningTimestamp) - - - // start/stop chains - val res = providerStateAfterSending.consumerStatus.StartStopConsumers( - consumersToStart, - consumersToStop, - timedOutConsumers - ) - val newConsumerStatus = res._1 - val err = res._2 - val providerStateAfterConsumerAdvancement = providerStateAfterSending.with( - "consumerStatus", newConsumerStatus - ).with( - "providerValidatorSetChangedInThisBlock", false - ) - - if (err != "") { - Err(err) - } else { - // for each consumer we just set to running, set its initial validator set to be the current one on the provider... - val valSet = providerStateAfterConsumerAdvancement.chainState.currentValidatorSet - val newConsumerStateMap = - tmpState.consumerStates.keys().mapBy( - (consumer) => - if (consumersToStart.contains(consumer)) { - // ...modified by the key assignments for the consumer - val consValSet = applyKeyAssignmentToValSet(providerStateAfterConsumerAdvancement, consumer, valSet) - val currentConsumerState: ConsumerState = tmpState.consumerStates.get(consumer) - val newConsumerState: ConsumerState = currentConsumerState.with( - "chainState", currentConsumerState.chainState.with( - "currentValidatorSet", consValSet - ).with( - "votingPowerHistory", - List(consValSet) - ).with( - "lastTimestamp", - providerStateAfterConsumerAdvancement.chainState.lastTimestamp - ).with( - "runningTimestamp", - providerStateAfterConsumerAdvancement.chainState.runningTimestamp - ) - ) - newConsumerState - } else { - currentState.consumerStates.get(consumer) - } - ) - val newState = tmpState.with( - "providerState", providerStateAfterConsumerAdvancement - ).with( - "consumerStates", newConsumerStateMap - ) - - Ok(newState) - } - } - - pure def endAndBeginBlockForConsumer( - currentState: ProtocolState, - chain: Chain, - // by how much the timestamp of the chain should be advanced for the next block - timeAdvancement: Time): Result = { - if (not(currentState.consumerStates.keys().contains(chain))) { - // if the chain is not a consumer, return an error - Err("chain is not a consumer") - } else { - val currentConsumerState: ConsumerState = currentState.consumerStates.get(chain) - val oldChainState: ChainState = currentConsumerState.chainState - val newChainState: ChainState = currentConsumerState.chainState.endAndBeginBlockShared(timeAdvancement) - val newConsumerState: ConsumerState = currentConsumerState.with( - "chainState", newChainState - ) - val maturedPackets = newConsumerState.maturationTimes.select( - pair => - val packet = pair._1 - val maturationTime: Time = pair._2 - // important: this uses the last committed timestamp, not the running timestamp! - maturationTime <= newChainState.lastTimestamp - ).transform(pair => pair._1) - val newMaturationTimes = newConsumerState.maturationTimes.select( - pair => - val packet = pair._1 - val maturationTime: Time = pair._2 - // important: this uses the last committed timestamp, not the running timestamp! - - maturationTime > newChainState.lastTimestamp - ) - val newOutstandingPackets = newConsumerState.outstandingPacketsToProvider.concat( - maturedPackets.transform( - packet => { - id: packet.id, - // it is important to use the oldChainState here, since this happens during EndBlock - sendingTime: oldChainState.runningTimestamp, - timeoutTime: oldChainState.runningTimestamp + CcvTimeout.get(chain) - } - ) - ) - val newConsumerState2: ConsumerState = newConsumerState.with( - "maturationTimes", newMaturationTimes - ).with( - "outstandingPacketsToProvider", newOutstandingPackets - ) - val newConsumerStates = currentState.consumerStates.set(chain, newConsumerState2) - val newState: ProtocolState = currentState.with( - "consumerStates", newConsumerStates - ) - Ok(newState) - } - } - - // Validator providerNode assigns their address for the consumer to be the consumerAddress. - pure def assignConsumerKey(currentState: ProtocolState, consumer: Chain, providerNode: Node, consumerAddr: ConsumerAddr): Result = { - // rule 1: validator A cannot assign consumer key K to consumer chain X - // if there is already a validator B (B!=A) using K on the provider - pure val provCurValSet = currentState.providerState.chainState.currentValidatorSet - if (provCurValSet.keys().exists(node => node != providerNode and node == consumerAddr)) { - Err("validator A cannot assign consumer key K to consumer chain X - if there is already a validator B (B!=A) using K on the provider") - } else { - // rule 2: validator A cannot assign consumer key K to consumer chain X if - // there is already a validator B using K on X - pure val valByConsAddr = currentState.providerState.consumerAddrToValidator.getOrElse(consumer, Map()) - if (valByConsAddr.keys().contains(consumerAddr)) { - Err("consumer key is already in use on the consumer chain") - } else { - // this key can be assigned - - // get the old assigned key - pure val consKeyByVal = currentState.providerState.validatorToConsumerAddr.getOrElse(consumer, Map()) - pure val p = if (consKeyByVal.keys().contains(providerNode)) { - // providerNode had previously assigned a consumer key - (consKeyByVal.get(providerNode), true) - } else { - (providerNode, false) - } - // the consumer address that was previously associated with the node - pure val oldConsAddr = p._1 - // whether the old address was explicitly assigned, or the default key - pure val prevAssigned = p._2 - - // set the old address for pruning, if it was assigned - pure val tmpState = if (prevAssigned) { - AppendConsumerAddrToPrune(currentState, oldConsAddr, consumer) - } else { - currentState - } - - // check whether the validator has positive power - pure val provValSet = currentState.providerState.chainState.currentValidatorSet - pure val provValPower = if (provValSet.keys().contains(providerNode)) provValSet.get(providerNode) else 0 - pure val consumersWithAddrAssignmentChangesInThisBlock = - if (provValPower > 0) { - // if the consumer has positive power, the relevant key assignment for the consumer changed - currentState.providerState.consumersWithAddrAssignmentChangesInThisBlock.union(Set(consumer)) - } else { - // otherwise, the consumer doesn't need to know about the change, so no change - currentState.providerState.consumersWithAddrAssignmentChangesInThisBlock - } - pure val tmpStateAfterKeyAssignmentReplacement = tmpState.with( - "providerState", tmpState.providerState.with( - "consumersWithAddrAssignmentChangesInThisBlock", consumersWithAddrAssignmentChangesInThisBlock - ) - ) - - pure val newvalidatorToConsumerAddr = currentState.providerState.validatorToConsumerAddr.put( - consumer, - currentState.providerState.validatorToConsumerAddr.getOrElse(consumer, Map()).put( - providerNode, - consumerAddr - ) - ) - - pure val newconsumerAddrToValidator = currentState.providerState.consumerAddrToValidator.put( - consumer, - currentState.providerState.consumerAddrToValidator.getOrElse(consumer, Map()).put( - consumerAddr, - providerNode - ) - ) - - pure val newProviderState = tmpStateAfterKeyAssignmentReplacement.providerState.with( - "validatorToConsumerAddr", newvalidatorToConsumerAddr - ).with( - "consumerAddrToValidator", newconsumerAddrToValidator - ) - - Ok( - tmpStateAfterKeyAssignmentReplacement.with( - "providerState", newProviderState - ) - ) - } - } - } - - // =================== - // UTILITY FUNCTIONS - // which do not hold the core logic of the protocol, but are still part of it - // =================== - - // Appends the key assignment for the given oldConsAddr on the consumer by a validator - // to be pruned when a VscMaturedPacket for the current runningVscId is received from the consumer. - pure def AppendConsumerAddrToPrune(currentState: ProtocolState, oldConsAddr: ConsumerAddr, consumer: Chain): ProtocolState = { - pure val vscId = currentState.providerState.runningVscId - pure val consumerAddrsToPrune = currentState.providerState.consumerAddrsToPrune.getOrElse(consumer, Map()) - pure val prevConsAddrs = consumerAddrsToPrune.getOrElse(oldConsAddr, Map()).getOrElse(vscId, []) - - pure val newConsAddrsToPrune = consumerAddrsToPrune.put(vscId, prevConsAddrs.append(oldConsAddr)) - - currentState.with( - "providerState", - currentState.providerState.with( - "consumerAddrsToPrune", - currentState.providerState.consumerAddrsToPrune.put(consumer, newConsAddrsToPrune) - ) - ) - } - - // Returns the new ConsumerStatusMap according to the consumers to stop - // and the consumers to time out. - // If a consumer is both stopped and timed out, it will be timed out. - // The second return is an error string: If it is not equal to "", - // it contains an error message, and the first return should be ignored. - pure def stopConsumers( - currentConsumerStatusMap: Chain -> str, - consumersToStop: Set[Chain], - consumersToTimeout: Set[Chain]): (Chain -> str, str) = { - val runningConsumers = currentConsumerStatusMap.keys().filter( - chain => currentConsumerStatusMap.get(chain) == RUNNING - ) - // all consumers to stop must be running right now, else we have an error - if (consumersToStop.exclude(runningConsumers).size() > 0) { - (currentConsumerStatusMap, "Cannot stop a consumer that is not running") - } else { - val newConsumerStatusMap = currentConsumerStatusMap.keys().mapBy( - (chain) => - if (consumersToTimeout.contains(chain)) { - TIMEDOUT - } else if (consumersToStop.contains(chain)) { - STOPPED - } else { - currentConsumerStatusMap.get(chain) - } - ) - (newConsumerStatusMap, "") - } - } - - // Returns the new ConsumerStatusMap according to the consumers to start. - // The second return is an error string: If it is not equal to "", - // it contains an error message, and the first return should be ignored. - pure def startConsumers( - currentConsumerStatusMap: Chain -> str, - consumersToStart: Set[Chain]): (Chain -> str, str) = { - val nonConsumers = currentConsumerStatusMap.keys().filter( - chain => currentConsumerStatusMap.get(chain) == NOT_CONSUMER - ) - // all consumers to start must be nonConsumers right now, otherwise we have an error - if (consumersToStart.exclude(nonConsumers).size() > 0) { - (currentConsumerStatusMap, "cannot start a consumer that is stopped or already a consumer") - } else { - val newConsumerStatusMap = currentConsumerStatusMap.keys().mapBy( - (chain) => - if (consumersToStart.contains(chain)) { - RUNNING - } else { - currentConsumerStatusMap.get(chain) - } - ) - (newConsumerStatusMap, "") - } - } - - pure def StartStopConsumers( - currentConsumerStatusMap: Chain -> str, - consumersToStart: Set[Chain], - consumersToStop: Set[Chain], - consumersToTimeout: Set[Chain] - ): (Chain -> str, str) = { - // check if any consumer is both started and stopped - if (consumersToStart.intersect(consumersToStop).size() > 0) { - (currentConsumerStatusMap, "Cannot start and stop a consumer at the same time") - } else { - val res1 = currentConsumerStatusMap.startConsumers(consumersToStart) - val newConsumerStatus = res1._1 - val err1 = res1._2 - val res2 = newConsumerStatus.stopConsumers(consumersToStop, consumersToTimeout) - val err2 = res2._2 - if (err1 != "") { - (currentConsumerStatusMap, err1) - } else if (err2 != "") { - (currentConsumerStatusMap, err2) - } else { - (res2._1, "") - } - } - } - - - // Takes the currentValidatorSet and puts it as the newest set of the voting history - pure def enterCurValSetIntoBlock(chainState: ChainState): ChainState = { - chainState.with( - "votingPowerHistory", chainState.votingPowerHistory.prepend( - chainState.currentValidatorSet - ) - ) - } + // Takes the currentValidatorSet and puts it as the newest set of the voting history + pure def enterCurValSetIntoBlock(chainState: ChainState): ChainState = { + chainState.with( + "votingPowerHistory", chainState.votingPowerHistory.prepend( + chainState.currentValidatorSet + ) + ) + } // Advances the timestamp in the chainState by timeAdvancement pure def advanceTime(chainState: ChainState, timeAdvancement: Time): ChainState = @@ -843,8 +418,12 @@ module ccv { // * increments the runningVscId // This should only be called when the provider chain is ending a block. // If no vsc packets need to be sent, this will be a noop. - pure def sendVscPackets(providerState: ProviderState, sendingTimestamp: Time): ProviderState = { - val newSentPacketsPerConsumer = ConsumerChains.mapBy( // compute, for each consumer, a list of new packets to be sent + // the ccv timeout should be the ccv timeout for the provider chain. + pure def sendVscPackets( + providerState: ProviderState, + sendingTimestamp: Time, + ccvTimeout: Time): ProviderState = { + val newSentPacketsPerConsumer = providerState.getRunningConsumers().mapBy( // compute, for each consumer, a list of new packets to be sent (consumer) => // if validator set changed or the key assignments for this chain changed, and the consumer is running, send a packet if ((providerState.providerValidatorSetChangedInThisBlock or @@ -860,20 +439,20 @@ module ccv { providerState.chainState.currentValidatorSet ), sendingTime: sendingTimestamp, - timeoutTime: sendingTimestamp + CcvTimeout.get(PROVIDER_CHAIN) + timeoutTime: sendingTimestamp + ccvTimeout }) } else { // no packet to be sent, so empty list List() } ) - val newOutstandingPacketsToConsumer = ConsumerChains.mapBy( + val newOutstandingPacketsToConsumer = providerState.getRunningConsumers().mapBy( (consumer) => providerState.outstandingPacketsToConsumer.getOrElse(consumer, List()).concat( newSentPacketsPerConsumer.get(consumer) ) ) - val newSentVscPackets = ConsumerChains.mapBy( + val newSentVscPackets = providerState.getRunningConsumers().mapBy( (consumer) => providerState.sentVscPacketsToConsumer.getOrElse(consumer, List()).concat( newSentPacketsPerConsumer.get(consumer) @@ -896,201 +475,634 @@ module ccv { } } - // receives a given packet (sent by the provider) on the consumer. The arguments are the consumer chain that is receiving the packet, and the packet itself. - // To receive a packet, modify the running validator set (not the one entered into the block yet, - // but the candidate that would be put into the block if it ended now) - // and store the maturation time for the packet. - pure def recvPacketOnConsumer(currentState: ProtocolState, receiver: Chain, packet: VscPacket): Result = { - if(not(isRunningConsumer(receiver, currentState.providerState))) { - Err("Receiver is not currently a consumer - must have 'running' status!") - } else { - // update the running validator set, but not the history yet, - // as that only happens when the next block is started - val currentConsumerState: ConsumerState = currentState.consumerStates.get(receiver) - val newConsumerState: ConsumerState = - { - ...currentConsumerState, - chainState: currentConsumerState.chainState.with( - "currentValidatorSet", packet.validatorSet - ), - maturationTimes: currentConsumerState.maturationTimes.append( - ( - packet, - currentConsumerState.chainState.runningTimestamp + UnbondingPeriodPerChain.get(receiver) - ) - ), - receivedVscPackets: currentConsumerState.receivedVscPackets.prepend(packet) + // receives a given packet (sent by the provider) on the consumer. The arguments are the consumer chain that is receiving the packet, and the packet itself, + // as well as the unbonding period for the consumer chain. + // To receive a packet, modify the running validator set (not the one entered into the block yet, + // but the candidate that would be put into the block if it ended now) + // and store the maturation time for the packet. + pure def recvPacketOnConsumer( + currentState: ProtocolState, + receiver: Chain, + packet: VscPacket, + receiverUnbondingPeriod: Time): Result = { + if(not(isRunningConsumer(receiver, currentState.providerState))) { + Err("Receiver is not currently a consumer - must have 'running' status!") + } else { + // update the running validator set, but not the history yet, + // as that only happens when the next block is started + val currentConsumerState: ConsumerState = currentState.consumerStates.get(receiver) + val newConsumerState: ConsumerState = + { + ...currentConsumerState, + chainState: currentConsumerState.chainState.with( + "currentValidatorSet", packet.validatorSet + ), + maturationTimes: currentConsumerState.maturationTimes.append( + ( + packet, + currentConsumerState.chainState.runningTimestamp + receiverUnbondingPeriod + ) + ), + receivedVscPackets: currentConsumerState.receivedVscPackets.prepend(packet) + } + val newConsumerStates = currentState.consumerStates.set(receiver, newConsumerState) + val newState = currentState.with( + "consumerStates", newConsumerStates + ) + Ok(newState) + } + } + + // receives a given packet on the provider. The arguments are the consumer chain that sent the packet, and the packet itself. + // To receive a packet, add it to the list of received maturations. + pure def recvPacketOnProvider(currentState: ProtocolState, sender: Chain, packet: VscMaturedPacket): Result = { + if (not(isRunningConsumer(sender, currentState.providerState))) { + Err("Sender is not currently a consumer - must have 'running' status!") + } else if (currentState.providerState.sentVscPacketsToConsumer.get(sender).head().id != packet.id) { + // the packet is not the oldest sentVscPacket, something went wrong + Err("Received maturation is not for the oldest sentVscPacket") + } else { + val currentReceivedMaturations = currentState.providerState.receivedMaturations + val newReceivedMaturations = currentReceivedMaturations.union(Set(packet)) + val newProviderState = currentState.providerState.with( + "receivedMaturations", newReceivedMaturations + ) + // prune the sentVscPacket + val newSentVscPacket = currentState.providerState.sentVscPacketsToConsumer.get(sender).tail() + val newState = currentState.with( + "providerState", + {...newProviderState, + sentVscPacketsToConsumer: currentState.providerState.sentVscPacketsToConsumer.set(sender, newSentVscPacket) + } + ) + + // prune assigned keys that are no longer needed + + // get the keys that should be pruned + pure val consumerAddrsToPrune = + currentState.providerState.consumerAddrsToPrune + .getOrElse(sender, Map()) + .getOrElse(packet.id, List()).toSet() + + // actually do the pruning + pure val senderValByConsAddr = + // for the sender chain, get the consAddrToValidator mapping + newState.providerState.consumerAddrToValidator.getOrElse(sender, Map()) + pure val newSenderValByConsAddr = + senderValByConsAddr.keys().filter( + // filter out the assigned keys that should be pruned + consAddr => not(consumerAddrsToPrune.contains(consAddr)) + ).mapBy( + // rebuild the map with the pruned keys removed + consAddr => senderValByConsAddr.get(consAddr) + ) + + // update the provider state + pure val newProviderState2 = newState.providerState.with( + // update the consumerAddrToValidator map to remove the pruned keys + "consumerAddrToValidator", + newState.providerState.consumerAddrToValidator.put(sender, newSenderValByConsAddr) + ).with( + // delete the consumer addresses to prune, since they are pruned now + "consumerAddrsToPrune", + newState.providerState.consumerAddrsToPrune.put( + sender, + newState.providerState.consumerAddrsToPrune.getOrElse(sender, Map()).put( + packet.id, + List() + ) + ) + ) + + // update the state + pure val newState2 = newState.with( + "providerState", newProviderState2 + ) + + Ok(newState2) + } + } + + // removes the oldest outstanding packet from the consumer. on-chain, this would happen when the packet is acknowledged. + // only the oldest packet can be removed, since we model ordered channels. + pure def removeOutstandingPacketFromConsumer(currentState: ProtocolState, sender: Chain): ProtocolState = { + val currentOutstandingPackets = currentState.consumerStates.get(sender).outstandingPacketsToProvider + val newOutstandingPackets = currentOutstandingPackets.tail() + val newConsumerState = currentState.consumerStates.get(sender).with( + "outstandingPacketsToProvider", newOutstandingPackets + ) + val newConsumerStates = currentState.consumerStates.set(sender, newConsumerState) + val newState = currentState.with( + "consumerStates", newConsumerStates + ) + newState + } + + // removes the oldest outstanding packet (to the given consumer) from the provider. + // on-chain, this would happen when the packet is acknowledged. + // only the oldest packet can be removed, since we model ordered channels. + pure def removeOutstandingPacketFromProvider(currentState: ProtocolState, receiver: Chain): ProtocolState = { + val currentOutstandingPackets = currentState.providerState.outstandingPacketsToConsumer.get(receiver) + val newOutstandingPackets = currentOutstandingPackets.tail() + val newProviderState = currentState.providerState.with( + "outstandingPacketsToConsumer", + currentState.providerState.outstandingPacketsToConsumer.set(receiver, newOutstandingPackets) + ) + val newState = currentState.with( + "providerState", newProviderState + ) + newState + } + + // Returns a ProtocolState where the current validator set on the provider is set to + // newValidatorSet. + pure def setProviderValidatorSet(currentState: ProtocolState, newValidatorSet: ValidatorSet): ProtocolState = { + pure val newChainState = currentState.providerState.chainState.with( + "currentValidatorSet", newValidatorSet + ) + currentState.with( + "providerState", + currentState.providerState.with( + "chainState", newChainState + ) + ) + } + + // Returns true if the given chain is currently a running consumer, false otherwise. + pure def isRunningConsumer(chain: Chain, providerState: ProviderState): bool = { + val status = providerState.consumerStatus.get(chain) + status == RUNNING + } + + // Returns the set of all consumer chains that currently have the status RUNNING. + pure def getRunningConsumers(providerState: ProviderState): Set[Chain] = { + providerState.consumerStatus.keys().filter( + chain => providerState.consumerStatus.get(chain) == RUNNING + ) + } + + // Returns the set of all consumer chains that currently have the status NOT_CONSUMER. + pure def getNonConsumers(providerState: ProviderState): Set[Chain] = { + providerState.consumerStatus.keys().filter( + chain => providerState.consumerStatus.get(chain) == NOT_CONSUMER + ) + } + + // Returns whether the consumer has timed out due to the vscTimeout, and an error message. + // If the second return is not equal to "", the first return should be ignored. + // If it is equal to "", the first return will be true if the consumer has timed out and should be dropped, + // or false otherwise. + pure def TimeoutDueToVscTimeout(currentState: ProtocolState, consumer: Chain, vscTimeout: Time): (bool, str) = + // check for errors: the consumer is not running + if (not(isRunningConsumer(consumer, currentState.providerState))) { + (false, "Consumer is not currently a consumer - must have 'running' status!") + } else { + val providerState = currentState.providerState + val consumerState: ConsumerState = currentState.consumerStates.get(consumer) + + // has a packet been sent on the provider more than vscTimeout ago, but we have not received an answer since then? + val sentVscPacketsToConsumer = providerState.sentVscPacketsToConsumer.get(consumer) + if(sentVscPacketsToConsumer.length() > 0) { + val oldestSentVscPacket = sentVscPacketsToConsumer.head() // if length is 0, this is undefined, but we check for this before we use it + if(oldestSentVscPacket.sendingTime + vscTimeout < providerState.chainState.runningTimestamp) { + (true, "") + } else { + // no timeout yet, it has not been vscTimeout since that packet was sent + (false, "") + } + } else { + // no packet has been sent yet, so no timeout + (false, "") + } + } + + +} + +module ccv { + // Implements the core logic of the cross-chain validation protocol. + + // Things that are not modelled: + // * Reward distribution + // * Slashes + + // Things that explicitly are modelled: + // * Validator set changes are propagated from provider to consumers + // * Vsc packets mature + + // We assume that packet receive + ack happen synchronously, + // i.e. when the packet is delivered, the ack is delivered right afterwards. + // This is because it is nontrivial in practice to get a relayer to relay a packet, but not its ack. + + import Time.* from "./libraries/Time" + import extraSpells.* from "./libraries/extraSpells" + import ccv_types.* + + + // =================== + // PROTOCOL PARAMETERS + // =================== + + // the set of all possible consumer chains. + const ConsumerChains: Set[Chain] + + // For each chain, this defines the time between the initiation of an unbonding and its maturation. + const UnbondingPeriodPerChain: Chain -> int + + // The maximum time duration between sending any VscPacket to any consumer chain and receiving the + // corresponding VscMaturedPacket, without timing out the consumer chain and consequently removing it. + const VscTimeout: int + + // The timeoutTimestamp for sent packets. Can differ by chain. + const CcvTimeout: Chain -> int + + // The trusting period on each chain. + // If headers on a channel between two chains are not updated within this period, + // they expire and the channel will be closed. + const TrustingPeriodPerChain: Chain -> int + + // =================== + // PROTOCOL LOGIC contains the meat of the protocol + // functions here roughly correspond to API calls that can be triggered from external sources + // =================== + + // the power of a validator on the provider chain is changed by the given amount. We do not care how this happens, + // e.g. via undelegations, or delegations, ... + pure def votingPowerChange(currentState: ProtocolState, validator: Node, amount: int): Result = { + pure val currentValidatorSet = currentState.providerState.chainState.currentValidatorSet + pure val oldVotingPower = if (currentValidatorSet.keys().contains(validator)) currentValidatorSet.get(validator) else 0 + if (amount == 0) { + Err("Voting power cannot change by zero!") + } else if (oldVotingPower == 0) { + Err("Voting power cannot be changed for a validator that is not in the current validator set!") + } else { + pure val newVotingPower = oldVotingPower + amount + pure val newValidatorSet = currentValidatorSet.put(validator, if(newVotingPower >= 0) newVotingPower else 0) + // if the new validator set contains no validators with positive voting power, the validator set is invalid + if (newValidatorSet.values().filter(votingPower => votingPower > 0).size() == 0) { + Err("Voting power change resulted in empty validator set!") + } else { + // set the validator set changed flag + val newProviderState = currentState.providerState.with( + "providerValidatorSetChangedInThisBlock", true + ) + pure val tmpState = currentState.with( + "providerState", newProviderState + ) + pure val newState = setProviderValidatorSet(tmpState, newValidatorSet) + Ok(newState) } - val newConsumerStates = currentState.consumerStates.set(receiver, newConsumerState) - val newState = currentState.with( - "consumerStates", newConsumerStates - ) - Ok(newState) - } + } } - // receives a given packet on the provider. The arguments are the consumer chain that sent the packet, and the packet itself. - // To receive a packet, add it to the list of received maturations. - pure def recvPacketOnProvider(currentState: ProtocolState, sender: Chain, packet: VscMaturedPacket): Result = { + // Delivers the next queued VscMaturedPacket from a consumer chain to the provider chain. + // Arguments are the currentState and the the consumer chain, from which the packet will be delivered. + // If this packet will time out on the provider on delivery, + // the consumer will be dropped. + // The first return is the result of the operation, the second result is a boolean + // that indicates whether the consumer timed out or not. + // If the result has an error, the second return should be ignored. + pure def deliverPacketToProvider(currentState: ProtocolState, sender: Chain): (Result, bool) = { if (not(isRunningConsumer(sender, currentState.providerState))) { - Err("Sender is not currently a consumer - must have 'running' status!") - } else if (currentState.providerState.sentVscPacketsToConsumer.get(sender).head().id != packet.id) { - // the packet is not the oldest sentVscPacket, something went wrong - Err("Received maturation is not for the oldest sentVscPacket") + (Err("Sender is not currently a consumer - must have 'running' status!"), false) + } else if (length(currentState.consumerStates.get(sender).outstandingPacketsToProvider) == 0) { + (Err("No outstanding packets to deliver"), false) } else { - val currentReceivedMaturations = currentState.providerState.receivedMaturations - val newReceivedMaturations = currentReceivedMaturations.union(Set(packet)) - val newProviderState = currentState.providerState.with( - "receivedMaturations", newReceivedMaturations - ) - // prune the sentVscPacket - val newSentVscPacket = currentState.providerState.sentVscPacketsToConsumer.get(sender).tail() - val newState = currentState.with( - "providerState", - {...newProviderState, - sentVscPacketsToConsumer: currentState.providerState.sentVscPacketsToConsumer.set(sender, newSentVscPacket) - } - ) - - // prune assigned keys that are no longer needed - - // get the keys that should be pruned - pure val consumerAddrsToPrune = - currentState.providerState.consumerAddrsToPrune - .getOrElse(sender, Map()) - .getOrElse(packet.id, List()).toSet() - - // actually do the pruning - pure val senderValByConsAddr = - // for the sender chain, get the consAddrToValidator mapping - newState.providerState.consumerAddrToValidator.getOrElse(sender, Map()) - pure val newSenderValByConsAddr = - senderValByConsAddr.keys().filter( - // filter out the assigned keys that should be pruned - consAddr => not(consumerAddrsToPrune.contains(consAddr)) - ).mapBy( - // rebuild the map with the pruned keys removed - consAddr => senderValByConsAddr.get(consAddr) + val packet = currentState.consumerStates.get(sender).outstandingPacketsToProvider.head() + if(packet.timeoutTime <= currentState.providerState.chainState.runningTimestamp) { + // drop consumer + val result = stopConsumers( + currentState.providerState.consumerStatus, + Set(), + Set(sender) ) - // update the provider state - pure val newProviderState2 = newState.providerState.with( - // update the consumerAddrToValidator map to remove the pruned keys - "consumerAddrToValidator", - newState.providerState.consumerAddrToValidator.put(sender, newSenderValByConsAddr) - ).with( - // delete the consumer addresses to prune, since they are pruned now - "consumerAddrsToPrune", - newState.providerState.consumerAddrsToPrune.put( - sender, - newState.providerState.consumerAddrsToPrune.getOrElse(sender, Map()).put( - packet.id, - List() + val newConsumerStatus = result._1 + val err = result._2 + if (err != "") { + (Err(err), false) + } else { + val newProviderState = currentState.providerState.with( + "consumerStatus", newConsumerStatus ) - ) - ) + val newState: ProtocolState = currentState.with( + "providerState", newProviderState + ) + (Ok(newState), true) // true because the packet timed out + } + } else { + // the packet has not timed out, so receive it on the provider + val result = recvPacketOnProvider(currentState, sender, packet) + val tmpState = result.newState + if (result.hasError()) { + (result, false) + } else { + (Ok(removeOutstandingPacketFromConsumer(tmpState, sender)), false) // false because the packet did not time out + } + } + } + } - // update the state - pure val newState2 = newState.with( - "providerState", newProviderState2 - ) + // Delivers the next queued VscPacket from the provider chain to a consumer chain. + // Arguments are the current state and the consumer chain, to which the packet will be delivered. + // If this packet will time out on the consumer on delivery, + // the consumer will be dropped. + // The first return is the result of the operation, the second result is a boolean + // that indicates whether the consumer timed out or not. + // If the result has an error, the second return should be ignored. + pure def deliverPacketToConsumer(currentState: ProtocolState, receiver: Chain): (Result, bool) = { + if (not(isRunningConsumer(receiver, currentState.providerState))) { + (Err("Receiver is not currently a consumer - must have 'running' status!"), false) + } else if (length(currentState.providerState.outstandingPacketsToConsumer.get(receiver)) == 0) { + (Err("No outstanding packets to deliver"), false) + } else { + val packet = currentState.providerState.outstandingPacketsToConsumer.get(receiver).head() + // check if the consumer timed out + if (packet.timeoutTime <= currentState.consumerStates.get(receiver).chainState.runningTimestamp) { + // drop consumer + val result = stopConsumers( + currentState.providerState.consumerStatus, + Set(), + Set(receiver) + ) - Ok(newState2) + val newConsumerStatus = result._1 + val err = result._2 + if (err != "") { + (Err(err), false) + } else { + val newProviderState = currentState.providerState.with( + "consumerStatus", newConsumerStatus + ) + val newState: ProtocolState = currentState.with( + "providerState", newProviderState + ) + (Ok(newState), true) // true because the packet timed out + } + } else { + // the packet has not timed out, so receive it on the consumer + val result = recvPacketOnConsumer(currentState, receiver, packet, UnbondingPeriodPerChain.get(receiver)) + val tmpState = result.newState + if (result.hasError()) { + (result, false) + } else { + (Ok(removeOutstandingPacketFromProvider(tmpState, receiver)), false) // false because the packet did not time out + } + } } } - // removes the oldest outstanding packet from the consumer. on-chain, this would happen when the packet is acknowledged. - // only the oldest packet can be removed, since we model ordered channels. - pure def removeOutstandingPacketFromConsumer(currentState: ProtocolState, sender: Chain): ProtocolState = { - val currentOutstandingPackets = currentState.consumerStates.get(sender).outstandingPacketsToProvider - val newOutstandingPackets = currentOutstandingPackets.tail() - val newConsumerState = currentState.consumerStates.get(sender).with( - "outstandingPacketsToProvider", newOutstandingPackets + + + /// Ends a block on the provider. This means that the current validator set is committed on chain, + /// packets are queued, and the next block is started. Also, consumers that have passed + /// the VscTimeout without responding to a pending vscpacket are dropped. + pure def endAndBeginBlockForProvider( + currentState: ProtocolState, + // by how much the timestamp should be advanced, + // i.e. the timestamp for the next block is oldTimestamp + timeAdvancement + timeAdvancement: Time, + // a set of consumers that were not consumers before, but should be set to running now. + consumersToStart: Set[Chain], + // a set of consumers that were running before, but should be set to stopped now. + // This argument only needs to contain "voluntary" stops - + // forced stops, e.g. because a consumer timed out, + // will be added automatically. + consumersToStop: Set[Chain]): Result = { + val currentProviderState = currentState.providerState + + // check for vsc timeouts + val timedOutConsumers = getRunningConsumers(currentProviderState).filter( + consumer => + val res = TimeoutDueToVscTimeout(currentState, consumer, VscTimeout) + res._1 + ) + + // for each consumer chain, apply the key assignment to the current validator set + val currentValSets = ConsumerChains.mapBy( + (consumer) => + currentProviderState.applyKeyAssignmentToValSet( + consumer, + currentProviderState.chainState.currentValidatorSet + ) + ) + // store the current validator set with the key assignments applied in the history + val newKeyAssignedValSetHistory = currentValSets.keys().mapBy( + (consumer) => + currentProviderState.keyAssignedValSetHistory + .getOrElse(consumer, List()) // get the existing history (empty list if no history yet) + .prepend(currentValSets.get(consumer)) // prepend the current validator set with key assignments applied ) - val newConsumerStates = currentState.consumerStates.set(sender, newConsumerState) - val newState = currentState.with( - "consumerStates", newConsumerStates + + // run the shared core chainState logic + val newChainState = currentProviderState.chainState.endAndBeginBlockShared(timeAdvancement) + val providerStateAfterTimeAdvancement = + {...currentProviderState, chainState: newChainState, keyAssignedValSetHistory: newKeyAssignedValSetHistory} + val tmpState = currentState.with( + "providerState", providerStateAfterTimeAdvancement ) - newState - } + - // removes the oldest outstanding packet (to the given consumer) from the provider. - // on-chain, this would happen when the packet is acknowledged. - // only the oldest packet can be removed, since we model ordered channels. - pure def removeOutstandingPacketFromProvider(currentState: ProtocolState, receiver: Chain): ProtocolState = { - val currentOutstandingPackets = currentState.providerState.outstandingPacketsToConsumer.get(receiver) - val newOutstandingPackets = currentOutstandingPackets.tail() - val newProviderState = currentState.providerState.with( - "outstandingPacketsToConsumer", - currentState.providerState.outstandingPacketsToConsumer.set(receiver, newOutstandingPackets) + // send vsc packets (will be a noop if no sends are necessary) + val providerStateAfterSending = + providerStateAfterTimeAdvancement.sendVscPackets( + currentProviderState.chainState.runningTimestamp, + CcvTimeout.get(PROVIDER_CHAIN) + ) + + + // start/stop chains + val res = providerStateAfterSending.consumerStatus.StartStopConsumers( + consumersToStart, + consumersToStop, + timedOutConsumers ) - val newState = currentState.with( - "providerState", newProviderState + val newConsumerStatus = res._1 + val err = res._2 + val providerStateAfterConsumerAdvancement = providerStateAfterSending.with( + "consumerStatus", newConsumerStatus + ).with( + "providerValidatorSetChangedInThisBlock", false ) - newState - } - // Returns a ProtocolState where the current validator set on the provider is set to - // newValidatorSet. - pure def setProviderValidatorSet(currentState: ProtocolState, newValidatorSet: ValidatorSet): ProtocolState = { - pure val newChainState = currentState.providerState.chainState.with( - "currentValidatorSet", newValidatorSet - ) - currentState.with( - "providerState", - currentState.providerState.with( - "chainState", newChainState + if (err != "") { + Err(err) + } else { + // for each consumer we just set to running, set its initial validator set to be the current one on the provider... + val valSet = providerStateAfterConsumerAdvancement.chainState.currentValidatorSet + val newConsumerStateMap = + tmpState.consumerStates.keys().mapBy( + (consumer) => + if (consumersToStart.contains(consumer)) { + // ...modified by the key assignments for the consumer + val consValSet = applyKeyAssignmentToValSet(providerStateAfterConsumerAdvancement, consumer, valSet) + val currentConsumerState: ConsumerState = tmpState.consumerStates.get(consumer) + val newConsumerState: ConsumerState = currentConsumerState.with( + "chainState", currentConsumerState.chainState.with( + "currentValidatorSet", consValSet + ).with( + "votingPowerHistory", + List(consValSet) + ).with( + "lastTimestamp", + providerStateAfterConsumerAdvancement.chainState.lastTimestamp + ).with( + "runningTimestamp", + providerStateAfterConsumerAdvancement.chainState.runningTimestamp + ) + ) + newConsumerState + } else { + currentState.consumerStates.get(consumer) + } + ) + val newState = tmpState.with( + "providerState", providerStateAfterConsumerAdvancement + ).with( + "consumerStates", newConsumerStateMap ) - ) - } - // Returns true if the given chain is currently a running consumer, false otherwise. - pure def isRunningConsumer(chain: Chain, providerState: ProviderState): bool = { - val status = providerState.consumerStatus.get(chain) - status == RUNNING + Ok(newState) + } } - // Returns the set of all consumer chains that currently have the status RUNNING. - pure def getRunningConsumers(providerState: ProviderState): Set[Chain] = { - providerState.consumerStatus.keys().filter( - chain => providerState.consumerStatus.get(chain) == RUNNING - ) - } + pure def endAndBeginBlockForConsumer( + currentState: ProtocolState, + chain: Chain, + // by how much the timestamp of the chain should be advanced for the next block + timeAdvancement: Time): Result = { + if (not(currentState.consumerStates.keys().contains(chain))) { + // if the chain is not a consumer, return an error + Err("chain is not a consumer") + } else { + val currentConsumerState: ConsumerState = currentState.consumerStates.get(chain) + val oldChainState: ChainState = currentConsumerState.chainState + val newChainState: ChainState = currentConsumerState.chainState.endAndBeginBlockShared(timeAdvancement) + val newConsumerState: ConsumerState = currentConsumerState.with( + "chainState", newChainState + ) + val maturedPackets = newConsumerState.maturationTimes.select( + pair => + val packet = pair._1 + val maturationTime: Time = pair._2 + // important: this uses the last committed timestamp, not the running timestamp! + maturationTime <= newChainState.lastTimestamp + ).transform(pair => pair._1) + val newMaturationTimes = newConsumerState.maturationTimes.select( + pair => + val packet = pair._1 + val maturationTime: Time = pair._2 + // important: this uses the last committed timestamp, not the running timestamp! - // Returns the set of all consumer chains that currently have the status NOT_CONSUMER. - pure def getNonConsumers(providerState: ProviderState): Set[Chain] = { - providerState.consumerStatus.keys().filter( - chain => providerState.consumerStatus.get(chain) == NOT_CONSUMER - ) + maturationTime > newChainState.lastTimestamp + ) + val newOutstandingPackets = newConsumerState.outstandingPacketsToProvider.concat( + maturedPackets.transform( + packet => { + id: packet.id, + // it is important to use the oldChainState here, since this happens during EndBlock + sendingTime: oldChainState.runningTimestamp, + timeoutTime: oldChainState.runningTimestamp + CcvTimeout.get(chain) + } + ) + ) + val newConsumerState2: ConsumerState = newConsumerState.with( + "maturationTimes", newMaturationTimes + ).with( + "outstandingPacketsToProvider", newOutstandingPackets + ) + val newConsumerStates = currentState.consumerStates.set(chain, newConsumerState2) + val newState: ProtocolState = currentState.with( + "consumerStates", newConsumerStates + ) + Ok(newState) + } } - // Returns whether the consumer has timed out due to the VscTimeout, and an error message. - // If the second return is not equal to "", the first return should be ignored. - // If it is equal to "", the first return will be true if the consumer has timed out and should be dropped, - // or false otherwise. - pure def TimeoutDueToVscTimeout(currentState: ProtocolState, consumer: Chain): (bool, str) = - // check for errors: the consumer is not running - if (not(isRunningConsumer(consumer, currentState.providerState))) { - (false, "Consumer is not currently a consumer - must have 'running' status!") + // Validator providerNode assigns their address for the consumer to be the consumerAddress. + pure def assignConsumerKey(currentState: ProtocolState, consumer: Chain, providerNode: Node, consumerAddr: ConsumerAddr): Result = { + // rule 1: validator A cannot assign consumer key K to consumer chain X + // if there is already a validator B (B!=A) using K on the provider + pure val provCurValSet = currentState.providerState.chainState.currentValidatorSet + if (provCurValSet.keys().exists(node => node != providerNode and node == consumerAddr)) { + Err("validator A cannot assign consumer key K to consumer chain X + if there is already a validator B (B!=A) using K on the provider") } else { - val providerState = currentState.providerState - val consumerState: ConsumerState = currentState.consumerStates.get(consumer) - - // has a packet been sent on the provider more than VscTimeout ago, but we have not received an answer since then? - val sentVscPacketsToConsumer = providerState.sentVscPacketsToConsumer.get(consumer) - if(sentVscPacketsToConsumer.length() > 0) { - val oldestSentVscPacket = sentVscPacketsToConsumer.head() // if length is 0, this is undefined, but we check for this before we use it - if(oldestSentVscPacket.sendingTime + VscTimeout < providerState.chainState.runningTimestamp) { - (true, "") + // rule 2: validator A cannot assign consumer key K to consumer chain X if + // there is already a validator B using K on X + pure val valByConsAddr = currentState.providerState.consumerAddrToValidator.getOrElse(consumer, Map()) + if (valByConsAddr.keys().contains(consumerAddr)) { + Err("consumer key is already in use on the consumer chain") + } else { + // this key can be assigned + + // get the old assigned key + pure val consKeyByVal = currentState.providerState.validatorToConsumerAddr.getOrElse(consumer, Map()) + pure val p = if (consKeyByVal.keys().contains(providerNode)) { + // providerNode had previously assigned a consumer key + (consKeyByVal.get(providerNode), true) } else { - // no timeout yet, it has not been VscTimeout since that packet was sent - (false, "") + (providerNode, false) } - } else { - // no packet has been sent yet, so no timeout - (false, "") + // the consumer address that was previously associated with the node + pure val oldConsAddr = p._1 + // whether the old address was explicitly assigned, or the default key + pure val prevAssigned = p._2 + + // set the old address for pruning, if it was assigned + pure val tmpState = if (prevAssigned) { + AppendConsumerAddrToPrune(currentState, oldConsAddr, consumer) + } else { + currentState + } + + // check whether the validator has positive power + pure val provValSet = currentState.providerState.chainState.currentValidatorSet + pure val provValPower = if (provValSet.keys().contains(providerNode)) provValSet.get(providerNode) else 0 + pure val consumersWithAddrAssignmentChangesInThisBlock = + if (provValPower > 0) { + // if the consumer has positive power, the relevant key assignment for the consumer changed + currentState.providerState.consumersWithAddrAssignmentChangesInThisBlock.union(Set(consumer)) + } else { + // otherwise, the consumer doesn't need to know about the change, so no change + currentState.providerState.consumersWithAddrAssignmentChangesInThisBlock + } + pure val tmpStateAfterKeyAssignmentReplacement = tmpState.with( + "providerState", tmpState.providerState.with( + "consumersWithAddrAssignmentChangesInThisBlock", consumersWithAddrAssignmentChangesInThisBlock + ) + ) + + pure val newvalidatorToConsumerAddr = currentState.providerState.validatorToConsumerAddr.put( + consumer, + currentState.providerState.validatorToConsumerAddr.getOrElse(consumer, Map()).put( + providerNode, + consumerAddr + ) + ) + + pure val newconsumerAddrToValidator = currentState.providerState.consumerAddrToValidator.put( + consumer, + currentState.providerState.consumerAddrToValidator.getOrElse(consumer, Map()).put( + consumerAddr, + providerNode + ) + ) + + pure val newProviderState = tmpStateAfterKeyAssignmentReplacement.providerState.with( + "validatorToConsumerAddr", newvalidatorToConsumerAddr + ).with( + "consumerAddrToValidator", newconsumerAddrToValidator + ) + + Ok( + tmpStateAfterKeyAssignmentReplacement.with( + "providerState", newProviderState + ) + ) } } + } // =================== // ASSUMPTIONS ON MODEL PARAMETERS