Automatic constraint generation and dynamic dispatch in GOOSE

This post attempts to explain the automatic RL constraint generation in GOOSE and how it affects dynamic dispatch.

Currently, the RL constraints associated with an object / class are derived from the constructors, destructors and methods the user has defined, plus explicitly specified extra constraints. In most cases, the user does not need to explicitly add any extra constraints. The automatically generated RL constraints require that the object is created / destroyed / modified with one of the constructors / destructors / methods defined in its class.

This arguably results in a user-friendly approach to class definitions. For example, when the user writes a Kudos class with a single transfer method which requires owner authorization, they would naturally expect that there is no other way of transferring the Kudos (for example with a new transfer method omitting the authorization check). Only the methods explicitly specified in the class definition can be used – other ways of modifying the object are not allowed.

class Kudos {
  owner : PublicIden
  originator : PublicIden
  denom : Denomination
  quantity : Nat
  
  constructor create(owner, denom, amount) : Kudos {
    return Kudos {
      owner := owner
      originator := owner
      denom := denom
      quantity := amount
    }
  }
  
  method transfer(newOwner : PublicIden) {
    check authorizedBy(self.owner)
    self.owner := newOwner
  }
}

The disadvantage of the implicit-constraints approach is that it makes dynamic dispatch and other dynamic aspects of OOP hard or impossible. Because we check whether a called method is “known”, we cannot add new ones in subclasses, or override existing ones.

For example, consider:

class KudosStore {
  owner : PublicIden
  store : Stack<Kudos>
  
  constructor create() {..}
  
  method insert(k : Kudos) {
    k.transfer(self.owner)
    self.store.push(k)
  }
}

------

class Kudos' : Kudos {
  override method transfer(newOwner : PublicIden) {
    ...
  }
}

If Kudos' is defined separately after defining Kudos and KudosStore, it is not possible to implement the above in a way which would allow Kudos' to be a subtype of Kudos so that it could be passed to KudosStore.insert and the right overridden Kudos'.transfer method would be called. This is because when compiling KudosStore we don’t know about the existence of the overridden Kudos'.transfer, so the generated RL cannot check for it.

We also cannot selectively allow the overriding of some methods while preventing the overriding of others. Once we open the possibility of allowing unknown calls in the object RL, arbitrary modifications of the object (modulo extra explicit constraints) can be submitted by a malicious attacker.

To see this more clearly, let’s look in more detail at how automatic constraint generation is done in GOOSE. The RL of the object checks if the received message is “known” and extra explicit per-object constraints are satisfied. The RL of the message checks if the object is modified in a way specified by the message / method. One could, of course, relax the RL of the object to allow any message. But then anything (modulo extra up-front constraints) can be done to the object because the RL of the message is not constrained.

In an explicit-constraints approach, the only thing that constrains the possible object changes are the up-front constraints the user who originally defines the object needs to come up with and make sure themselves that they fit all possible future situations. One could pursue this approach, but it seems less user-friendly because it is easy to forget about some constraints. In the implicit-constraints approach, simply no object modification is allowed that doesn’t correspond to one of the defined methods. In the explicit-constraints approach, the burden of coming up with the right constraints (simultaneously sufficiently general and restricted) is shifted to the user.

The explicit-constraints approach is compatible with GOOSE - one could simply indicate with some keyword to omit the known message check in the RL for a given class. The disadvantage is the necessity of coming up with explicit constraints. The advantage is that “dynamic” aspects of OOP become possible (method overriding, dynamic dispatch, etc).

1 Like

Why exactly would the generated RL (for a Kudos' resource) not be able to check for it? Shouldn’t we be able to encode the transfer(PublicIden) message in a way such that a new implementation can be defined later as long as the method signature doesn’t change (since the message encoding would remain the same)?

Somehow to me this seems slightly odd – could we instead encode the checks so that:

  • The RL of the object checks if the received message is known, the object is modified in a way specified by the message/method, and any extra explicit per-object constraints are satisfied.
  • The RL of the message just checks that the object has received the message (or sent it, in the case of sent messages). This shouldn’t require anything more than checking that the object-associated resource (with the right object identifier) is present in the action (since the object-associated resource logic will check all the messages in that action).

Then it should be possible to override methods etc. and just change object resource logics, without requiring any changes to message resource logics.

1 Like

Somehow to me this seems slightly odd – could we instead encode the checks so that:

  • The RL of the object checks if the received message is known, the object is modified in a way specified by the message/method, and any extra explicit per-object constraints are satisfied.
  • The RL of the message just checks that the object has received the message (or sent it, in the case of sent messages). This shouldn’t require anything more than checking that the object-associated resource (with the right object identifier) is present in the action (since the object-associated resource logic will check all the messages in that action).

This is essentially what we had in the previous version of GOOSE, except that messages were encoded in AppData instead of separate resources.

Then it should be possible to override methods etc. and just change object resource logics, without requiring any changes to message resource logics.

I see what you mean. I think this could work. I don’t know, I guess I got too fixated on the idea of associating the message logic with the message resource, but it’s indeed not necessary.

The difference is that then the meaning of the message is determined by the object that receives it, not by the message itself. But that’s a valid point of view closer to mainstream OOP, and indeed it seems necessary for dynamic dispatch. If we want some extra constraints on the meaning of the message, we can always specify them in the super-class and put those in the message RL.

So by default the message RLs wouldn’t do anything. We still need message resources to balance sent and received messages cross-action.

1 Like

Yes, exactly!

Yes. In some ways the machinery of the RM is a bit overkill for this and we might want to think of ways to optimize it over time, but it should certainly be sufficient.