Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add (immutable) auxiliary data to Presences? #2069

Open
warner opened this issue Dec 8, 2020 · 25 comments · May be fixed by #6355
Open

add (immutable) auxiliary data to Presences? #2069

warner opened this issue Dec 8, 2020 · 25 comments · May be fixed by #6355
Assignees
Labels
enhancement New feature or request marshal package: marshal needs-design SwingSet package: SwingSet

Comments

@warner
Copy link
Member

warner commented Dec 8, 2020

What is the Problem Being Solved?

Pass-by-presence objects are currently (almost) pure behavior. If you hold a Presence, the (almost) only thing you can do with it is:

  • send it a message
  • compare it for identity against some other Presence
    • e.g. use it as the key of a Map or WeakMap

The one partial exception is that we associate an "Interface name" (a sort of alleged type) with the Presence, which can be retrieved with a helper function (#1816).

On the other hand, if you send a pass-by-copy object (data but no behavior), it behaves like a normal javascript object, except you can't send it messages, and you can't compare it for identity (each act of transmission results in a new copy of the object.. if we could, we'd probably want to throw an error if you ever tried to use === on it, or use it as the key of a Map/WeakMap).

I've wondered if it would make development easier if we added immutable data to pass-by-presence copy objects. This would subsume the current getInterfaceOf(presence) call (you'd just do presence.interface instead). The vat which creates the Remotable could add whatever non-Function properties it likes, as long as the object is harden()ed, and the non-Function properties would be serialized as usual and attached to all copies of the Presence.

This might allow (alleged) Brands to be attached to Purses/Payments, instead of requiring a getBrand() roundtrip. We could add descriptive names to objects, for debugging.

This relates to:

Description of the Design

My notion is:

  • we add a "data" column to the kernel object table, which contains a capdata-serialized Object (mapping property name to value)
  • vats and kernels do not automatically supply the data to their recipient, instead the recipient asks for a copy if they don't already have it. Vats can use syscall.getDataForObject to retrieve the auxililary data the first time they receive an object, and the kernel can do dispatch.getDataForObject to fetch it from a vat shortly after the vat exports it for the first time
    • this is a tradeoff. I can think of three basic ways to do it: "always include data", "ask for data if you don't already have it", and "try to guess whether they already have the data". The latter would be the most efficient, but I'm not convinced we can correctly make both sides of all kernel/vat boundaries get this right.
  • since the data is supposed to be immutable, it ought to be stable: if a vat is asked for data and it returns something different than it did previously, that ought to be a vat-fatal error (although if we're asking them a second time, we probably don't remember enough about the first time to notice)

Extra feature: the auxilliary data could include additional references (to other objects and/or promises). These would represent new edges in the kernel-side object graph, which influences GC. I think this would be particularly useful for a Brand.

Security Considerations

This would be an additional way for vats to introduce reference cycles into the kernel data structures (they can already do so with mutually-referential promise resolutions), which may be hard to clean up: cycles require a full mark-and-sweep GC pass, rather than merely watching for refcounts to drop to zero.

This would make it harder to implement mostly-transparent proxy wrappers, such as a recovable forwarder, since the wrapper would need to enumerate all the auxilliary data and copy it (in wrapped form) onto the new proxy. A common bug would be to forget to copy the data properties, leading to a wrapper that behaves like the original but looks bare.

Open Questions

Does this seem useful? @erights @katelynsills @Chris-Hibbert are there ERTP things that would be easier or more ergonomic to express if you could do property lookups on your Presence objects?

For me, the high-order question is whether this auxilliary data can contain object/promise references, or if it is restricted to purely pass-by-copy data. If so, the kernel object table will be a source of GC references. I don't think it's significantly more work for me (in #1872) to accomodate this, since the kernel promise table is already a source, but I do wonder how much I should plan to accomodate it.

Does the "ask if you don't already have it" protocol feel right? We could explore it more, but my hunch is that having both sides know for sure whether the other side already knows about the data would complicate the GC protocol.

@warner warner added enhancement New feature or request SwingSet package: SwingSet labels Dec 8, 2020
@Chris-Hibbert
Copy link
Contributor

are there ERTP things that would be easier or more ergonomic to express if you could do property lookups on your Presence objects?

The Brand, as you mentioned, is the main one.

The other uses I've thought about that this would address are alleged type info, and tagging for debugging and tracing. Those don't seem to need live objects.

Does the "ask if you don't already have it" protocol feel right?

Yes, though allowing "try to guess" seems likely to be a good way to improve performance in some cases.

@katelynsills
Copy link
Contributor

I've wondered if it would make development easier if we added immutable data to pass-by-copy objects.

This part confused me because the next sentence talks about pass-by-presence objects. Should this be pass-by-presence?

Also, is there a reason to limit the effects to ERTP? What about Zoe? I could see some uses there

It seems like the criteria for use are:

  1. The object that the original, to-be-replaced method is on, must be a presence
  2. The result of that method must never change
  3. The method must be a getter only, and can take no parameters since there will not actually be a round-trip.

If the above criteria are correct, I think we could use it in the following ways in ERTP:

Issuer

issuer.getAllegedName() - returns a string - yes, this would be helpful
issuer.getAmountMathKind() - returns a string - yes this would be helpful
issuer.getAmountOf(payment) - will change, not helpful
issuer.getBrand() - returns a brand presence - yes this would be helpful
issuer.makeEmptyPurse() - will change, not helpful
issuer.burn() - not helpful
issuer.claim() - not helpful
rest of issuer methods - not helpful

Mint

mint.getIssuer() - returns an issuer presence - yes, this would be helpful
mint.mintPayment(newAmount) - not helpful

Brand

brand.isMyIssuer() - not helpful, not a getter
brand.getAllegedName() - returns a string - yes, this would be helpful

Purse

deposit - no
withdraw - no
getCurrentAmount - no
getCurrentAmountNotifier - yes, this would be helpful, there is one notifier per purse
getAllegedBrand - returns a brand presence - yes
getDepositFacet - returns a depositFacet presence - yes

Payment

getAllegedBrand - returns a brand presence - yes

AmountMath

Shouldn't be a presence (users should have localAmountMath to preclude a round-trip)

@warner
Copy link
Member Author

warner commented Dec 11, 2020

I've wondered if it would make development easier if we added immutable data to pass-by-copy objects.

This part confused me because the next sentence talks about pass-by-presence objects. Should this be pass-by-presence?

Oops, you're absolutely right. I'll update the comment.

Also, is there a reason to limit the effects to ERTP? What about Zoe? I could see some uses there

Nope, no reason to limit it, I should have asked about both (and I'm sure there's lots of contract code which might benefit from this feature).

It seems like the criteria for use are:

1. The object that the original, to-be-replaced method is on, must be a presence
2. The result of that method must never change
3. The method must be a getter only, and can take no parameters since there will not actually be a round-trip.

Yeah, any place where you're currently calling presence.getFoo() could be replaced with presence.foo.

I might add another criteria.. getFoo() is lazy, but .foo is not, so if it's expensive to provide, and we don't think it will be called very often, then we might want to leave the explicit method in place.

Also, the correctness of the auxilliary data depends upon the source of the Presence. You need to know that the Payment is real before you can believe anything else its data claims. If Carol exports a Remotable with some data, to Alice, and then Alice sends her Presence to Bob, the thing that Bob receives is entirely under Alice's control (both identity and auxilliary data). If Bob receives the same Presence from Carol, then he can believe that both Alice and Carol observe the same auxillary data. Alice cannot deliver an Presence to Bob that is EQ the one he got from Carol but has different auxillary data.

issuer.getAmountOf(payment) - will change, not helpful

Right, any property whose value is mutable wouldn't qualify for inclusion in aux data.

issuer.getBrand() - returns a brand presence - yes this would be helpful
mint.getIssuer() - returns an issuer presence - yes, this would be helpful
brand.getAllegedName() - returns a string - yes, this would be helpful

Yeah those seem like the biggest potential improvements.

Purse

getCurrentAmountNotifier - yes, this would be helpful, there is one notifier per purse

This might fall into the category of "desireably lazy".. I'm guessing we always build the Notifier for each Purse, regardless of whether someone calls getCurrentAmountNotifier for it, but if there's a cost to unconditionally exporting it, then it might be cheaoer to build/export it only on demand.

getDepositFacet - returns a depositFacet presence - yes

Do we think most Purses will have their deposit facets used? Or is this a lesser-used feature. Might fall into the lazy category too.

AmountMath

Shouldn't be a presence (users should have localAmountMath to preclude a round-trip)

Is this a case where we really want migratory code? I think AmountMath is basically a set of powerless functions that you want to be able to invoke cheaply/locally/synchronously. In theory I might send you an AmountMath by sending you a string which you then eval() (without providing any endowments). If so, then yeah we'll need some other abstraction to manage and migrate things like this.

@warner
Copy link
Member Author

warner commented Dec 12, 2020

@erights and I walked through this today. On the spectrum of "send-every-time" vs "guess" vs "ask", he's inclined to use send-every-time, and then look for a way to optimize things (and I expect @dtribble to prefer "guess", but he's more confident in our ability to guess correctly than me, especially in the face of GC possibly causing one side to forget the data without the other side realizing it).

We have two threats to protect against. One is equivocation: the exporting vat (holding the original Remotable) should not be able to present the same object with different auxilliary data to two different parties. The second is replacement: if Alice forwards Carol's object+data to Bob, Alice should not be able to substitute her own data into what Bob gets.

To prevent equivocation, we must either ensure that the exporting vat only exports the aux data exactly once (not giving it an opportunity to equivocate), or we must somehow detect that the second+subsequent copies of the data are equivalent to the first.

To compare the data against an earlier copy, the easiest approach is probably for the vat to remember the exact capdata it first serialized (probably as an extension of valToSlot). We could also seek to make marshaling deterministic, which is useful for other reasons. The challenges with deterministic marshaling are:

  • If the data contains Promises, they might become resolved, which means we've retired their vrefs, which means they'll be serialized into a different vpid, which looks like equivocation
    • We may need to exclude Promises from auxillary data
  • We must exclude unregistered Symbols as property names: they are unforgeable and have no comparable (sortable) identity, so we have no basis for ordering one before another. Registered symbols are associated with their (string) name, so those can be sorted just fine.
  • If/when we start serializing collections (Map and Set: mechanism to marshal immutable Set/Map #16, marshal cannot serialize standard Maps #838), and the collection includes an Object, we'd need some way to sort one object before/after another.
    • @erights suggested that sameStructure and sameKey, which are currently synonyms, might change. Auxilliary data (or anything that must be deterministically serializable) needs the keys to be Comparable, but not all Comparables are valid keys.

Another option is for the kernel to demarshal the data received from the vat, and compare it against a demarshalled copy of what the kernel remembered from before, but we both agreed we'd rather keep marshal out of the kernel, and have it stick to capdata.

As a starting point, we would probably allow aux data to include anything that is pass-by-copy, plus Presences (which are Comparable), and excluding Promises entirely.

A particularlly interesting idea was to define the identity of an object (its vref) to include the hash of its auxdata. The hope would be that anyone who receives a reference to the object could compare the alleged auxdata against the hash, to make sure they're receiving a correct copy. There are a whole mess of fiddly bits in the way (translation of vrefs into different vrefs by the time it arrives at a subscriber vat, cross-machine refs needing to include a canonical machine ID but chain-based machines have an evolving predicate instead of a single pubkey, separation of swissnum-style access control from c-list access control), but in a different environment the approach could be pretty tidy.

@warner warner added the marshal package: marshal label Feb 1, 2021
@warner
Copy link
Member Author

warner commented Feb 2, 2021

We had another good meeting on this today. What I learned:

  • A basic (albeit insufficient) way of describing auxilliary data is to imagine additional properties added to a Presence whose values are decided by the creator of the corresponding Remotable.
    • From a security point of view, using those (local, immediate) properties should be as reliable as eventual-sends to the owner of the Remotable. It is as if the client had a way to magically replace foo = await presence~.getFoo() with a synchronous foo = presence.foo. No matter which other vats the Presence has travelled through, if presence1 === presence2, then presence1.foo is deep-equal to presence2.foo.
  • The initial use case is to add an amount-math type indicator to Brand Presences.
    • Clients will receive an "Amount", which is a record containing some sort of quantity (a number, or a list of non-fungible token identifiers), plus a Brand, which is a Presence owned by the Issuer.
    • Different kinds of Amounts have different rules for addition (merging for Sets), subtraction (intersection of some sort), and comparison. We have roughly three types of rules right now.
    • Each type of amount math has a name (a string). All clients import the same (pure) library, which has a function to map this string to a set of functions that implement addition/etc for that type.
    • Our current/old approach is to ask the Brand for its amount math type (which requires a roundtrip), feed the resulting string into the library, and get back a set of functions which can be used to do math on the Amount.
  • The preferred client API is to receive a Brand Presence that has the amount math methods built-in, rather than requiring external functions. Merely adding a .amountMathType property to the Brand Presence is not sufficiently ergonomic.
    • So clients would receive amount = { quantity, brand }, and could do amount.brand.add(otheramount), perhaps after checking that amount.brand === expectedBrand.
    • In @erights 's experiments, the sender uses the second argument (props=) of Remotable to specify a math-type string, and the receiver's liveslots is entangled with the ERTP library to parse the string and Object.defineProperties the right methods onto it. (Actually it's even hackier than that, the interface name field is overloaded to include a JSON-serialized record with the brand information).
    • This approach violates the abstraction boundary between vats/liveslots and ERTP far to much for my tastes.
    • Also, the sender sees this data in a very different way than the recipient.
  • This is a limited/degenerate form of mobile code.
    • In the more general form, the Remotable definition could (maybe) provide arbitrary methods or properties, which would be serialized (as code) and sent to the client, which would evaluate that code locally and attach the resulting function/method to the Presence.
    • The obvious problem with anything that general is termination guarantees: the sender could include an infinite loop. The more interesting problem is comity: if the original code closes over authorities in the sender, what should the reconstructed version on the recipient get access to?
    • We aren't going to implement the general approach, but thinking about it in those terms might help us find a reasonable API.

I think we can split this up into two pieces: a lower-level facility for one vat to associate auxdata with a Presence (and deliver it correctly to recipient vats), and a higher-level facility for defining that auxdata (on the Remotable) and converting it into pre-specified behavior on the resulting Presence. The lower-level facility takes place in the deliveries and syscalls used by liveslots and the kernel. The higher-level facility lives somewhere between marshal, liveslots, and some sort of registration functions exposed by liveslots (perhaps in vatPowers) and called by user-level code that knows about ERTP and Brands and AmountMath and such.

Lower-level auxdata facility

I can see two rough categories of approaches to the lower-level facility:

  • include the auxdata in the serialized capdata somehow
  • deliver the auxdata through a separate channel

We have three properties of interest:

  • integrity/authenticity: a vat which receives a remote object (o-NN) that contains some auxdata should not be able to successfully send it to another vat with different auxdata
  • availability: any vat that receives auxdata-bearing remote objects should also receive the auxdata itself in a timely fashion
  • performance: it would be nice to avoid redundant delivery of auxdata to a recipient that already has a copy
    • for now, the auxdata is very small, and we don't anticipate the size becoming significant for a while

Also, it's worth pointing out that the current use case only needs "pure" auxdata: deeply pass-by-copy (so no Presences). The Brand would carry auxdata containing the String name of the amount math type, rather than e.g. a Presence that represents the amount math type. @erights said we don't currently have a need for Presence-bearing auxdata, but in the future it will become very important. I had thought that a Purse or Payment having a .brand property would be an obvious candidate, but @katelynsills pointed out that in all places where we might receive that, we don't want to trust the sender (we don't leave Purses lying around on the floor; we keep them organized by Issuer, so the need never really arises).

Auxdata in serialized capdata

marshal's serialize() method returns "capdata": a String "body", and an Array of "slots" (each of which is a short identifier like o+5 or p-12). The body is the output of JSON.stringify using a special "replacer" to handle special things. In cooperation with a convertValToSlot function provided to marshal, this converts Remotables into a magic JSON structure that looks like { "@qclass": "slot", "index": 0 }, where the index points into the slots array. The kernel does not examine or modify the body: it is passed verbatim through the run-queue and into whatever vat or vats receive the data (i.e. the arguments of an eventual-send, or the resolution of a promise). The kernel does modify the slots by mapping it through a c-list at each transition from vat to kernel, kernel to vat, or comms vat to remote machine.

In this first category of approaches, we have marshal put the auxdata in the serialized capdata. Every act of serialization will include a copy of the auxdata, which ensures availability (at the minor cost of performance). To maintain integrity, we must ensure that a vat which receives/imports an object cannot successfully re-emit it with different auxdata than what it received. We also want to prevent the originating vat from emitting the same object with two different auxdata values. There are a few different ways to do this.

  • 1: put the auxdata in the slots array: a slot like o+4 becomes more like o+4:${auxdata}
    • object identity depends upon the entire slot, so an attempt to equivocate about the auxdata results in two distinct objects
    • feels pretty messy: large slot identifiers run counter to the intention of the c-list
  • 2: put auxdata in the body, put a secure consistent hash of the auxdata in the slots array
    • a slot like o+4 becomes more like o+4:HASH
    • the kernel would reject vat misbehavior by parsing the body for @qclass: "slot", extracting and hashing their capdata, and comparing it against the hash in the slot
    • the kernel object table remembers the HASH for each object
  • 3: put auxdata in the body, have the kernel remember the auxdata for each object, kernel parses each capdata from the vat and compares against the recorded auxdata

Hashing auxdata becomes a significant problem if/when that data is allowed to to contain Presences, because their identifiers change depending upon the context. We do not have universal strong identifiers for Remotables, unfortunately, and the problem is made even more challenging by the mutable+evolving nature of the light-client predicate used to name a blockchain-hosted swingset machine. In a world where each swingset is identified by a stable public key, we could use ${pubkey}:${kernelObjectID} as a universal identifier, but that's not what we have (and wouldn't enable key-rotation anyways).

In these approaches, the kernel is nominally unaware of the auxdata (it's just a new part of the serialized capdata, which the kernel usually ignores. But the kernel does need to be involved to protect integrity.

It might be possible to maintain the kernel's ignorance by having all vats keep an internal objectID-to-auxdata mapping, and check for equivocation each time they receive (and deserialize) an object, rather than having the kernel be solely responsible for this check. In this case, the first time an auxdata-bearing object arrives in a vat is the defining occurrence, and establishes the auxdata for that Presence. If the same object-id is named in a subsequent message (perhaps from a different vat) that contains different auxdata, the receiving vat knows that something is wrong. It has no way to tell which copy is correct. It's not clear how the vat ought to surface this uncomfortable realization.

Auxdata in separate channel

Instead, I think we should make the kernel more aware of auxdata, and manage it outside the serialized capdata. The kernel object table, which currently maps kernelObjectID to ownerVatID, would acquire a third column with the capdata structure for the auxdata. And I'm thinking about an additional set of syscall/dispatch methods, to deliver the auxdata associated with an object-id.

If we wanted to minimize space overhead, especially for large auxdata, we could have the receiver ask for an auxdata it doesn't already remember. E.g. if the kernel does dispatch.deliver() and the arguments cite some new object, the liveslots layer could do an immediate syscall.getAuxData(objID) to populate the Presence, before giving control to user-level vat code. We could introduce some similar mechanism for the vat-to-kernel direction, for when the vat makes a syscall that introduces an object into the kernel. Perhaps the kernel gathers a list of objects for which it doesn't know the auxdata, and once the main crank is done, it turns around and performs an immediate dispatch.getAuxDataFor(objID) crank (which would not give user-level vat code a chance to run, and would be complete before any other vat gets a crank, so the kernel can populate the kernel tables with the data before anything else gets a chance to look at it).

@erights expressed concern about the latency of this approach, both for the sub-millisecond inter-process pipes we're using for the XS vat worker, and significantly moreso for the 10+-second delay between remote machines over IBC. And since we expect auxdata to be small, it would be better to just include the auxdata every time.

With enough cleverness and coordination, the two sides of any given link might be able to model the other side's state closely enough to know when/if they have the auxdata, and only pass it when they need it. This would minimize both latency and overhead, but would introduce more coupling between the two sides than I care for.

If every delivery includes a copy of the auxdata, maybe we could change the signatures of e.g. dispatch.deliver(targetObjectID, resultPromiseID, methodName, argCapData) to include an additional auxDataFor argument, which would contain a record mapping objectID to the auxdata capdata for it. If/when auxdata can contain Presences, since those Presences might themselves have auxdata, we'd need to build this record with a graph traversal of the kernel object table rows, to make sure that every reachable bit of auxdata was included (so auxDataFor would contain data for all presences in argCapData.slots, as well as all presences in auxDataFor[*].slots). We'd change dispatch.notify, syscall.send, and syscall.resolve in similar ways: every vat/kernel API that takes a capdata argument would also get an auxdata argument. We'd also enhance the comms protocol to include the same auxdata on cross-machine messages. All of these APIs would need to translate the auxdata keys through a c-list, as well as the slots referenced by the auxdata itself.

In this approach, the kernel would prohibit equivocation by comparing every single field of received auxdata against the new column of the kernel object table. Any disagreement would be a vat-vatal violation.

One equivocation that might be allowed would be a vat which exports an object with auxdata-1, waits until everyone else drops it (even the kernel), then re-exports the same object-id with auxdata-2. This probably wouldn't cause any problems, because nobody else could tell that the auxdata had changed. However, a system in which the auxdata was hashed to form the object-id would not even tolerate this.

@katelynsills
Copy link
Contributor

@warner, I just talked to @erights, and we agreed that we probably don't need to add the amountMath methods to brands before Beta. Instead, with just the mathKind property on the brand presence, we can refactor amountMath to be directly importable and have it dispatch to the correct mathHelpers under the hood. Here's the new design for AmountMath #2311 with a draft refactoring here: #2310

This way, even if we did move the amountMath methods to the brand (for the record, I am skeptical of this 😄 because then we lose one of the checks that amountMath does now, which is to check that the brand is equal to the brand you get from the issuer), the code that the user would write would remain unchanged, and we would merely change the amountMath implementation under the hood.

@warner
Copy link
Member Author

warner commented Feb 2, 2021

high-level facility to add local behavior to Presences

As I learned this morning, the real delivery of this feature is to add methods to the received Presence based upon the auxdata. I think we need a mechanism whereby the receiver can register collections of methods to add if triggered by a particular auxdata key. Perhaps something like:

const helpers = harden({
  nat: { add, equal, GTE, .. },
  set: { add, equal, GTE, .. },
});

function buildRootObject(vatPowers) {
  const { registerAuxdataEnhancer } = vatPowers;
  function addMathHelpers(name) {
    return helpers[name] || {};
  }
  registerAuxdataEnhancer('mathHelpersTypeName', addMathHelpers);
  ..
}

The idea would be that any incoming object with auxdata that contains a property named mathHelpersTypeName would invoke the registered addMathHelpers function, passing it the associated property value. Whatever record the enhancer returns would be merged into the Presence before being hardened.

This obviously needs a lot of thought:

  • the enhancements would override remote calls to the same name, is that an authority which could be misused?
  • probably yes, so putting the registration function on vatPowers might help to limit it somewhat
  • the enhancements can close over additional state, which could be used to provide a communication channel, is that ok?
  • if not, should they be defined as a string of code, to be evaluated independently for each new Presence?
  • the enhancements don't get special access to any per-Presence state, is that a hardship or impediment to some interesting use case?
  • the added functions wouldn't get access to the Presence object, unless we established a convention for it (caller does presence.foo(1, 2), enhancement function is invoked as foo(presence, 1, 2), which limits the utility somewhat

I imagine one of the MathHelper use cases is to transform something like:

const mathHelperTypeName = await oldBalance.brand~.getMathHelperType();
const math = knownHelpers.get(mathHelperTypeName);
const remaining = math.subtract(oldBalance, withdrawalAmount);

into:

const remaining = oldBalance.something.subtract(withdrawalAmount);

(i.e. if the enhancement is a method of the auxdata-bearing Presence, rather than merely an associated function, then it can take a more active role, and we could remove a somewhat-redundant argument from the call).

(or maybe that's most interesting if we're talking about adding behavior to data, rather than adding data to behavior, as @FUDCo pointed out)

Of course, this is really wandering into mobile-code territory. We might consider a subset of that which avoided the questions of comity and termination by limiting the code to specific strings pre-registered (by the recipient).

origin/exporter -side API

How should the creator of the Remotable express their interest in adding auxdata, or methods, to any resulting Presences? For simple auxdata that arrived as data properties on the Presence object, I think we could either pass them in the second argument to Remotable(iface, properties, methods), or simply accept data properties on otherwise passByCopy objects (which currently reject anything that's not a Function). But to indicate a set of methods, we need something else. It should use some string tag that lives in e.g. ERTP, which both sides can import, so if they import compatible versions of ERTP then they'll get matching versions of the math helpers.

It'd be nice if the Remotable-side API were somehow similar to the Presence-side API. For example, if the signature was Remotable(auxData, localFunctions, remoteMethods), then:

  • auxData would be the data properties that get copied onto the Presence
  • localFunctions are looked up in a table, mapped to a string known by both sides, and included in a special auxdata key. The receiving side looks up the string in its own table, finds a corresponding set of functions, and glues them into the Presence, either with or without access to the Presence itself, or some per-Presence local state only available to the glued-in functions
  • remoteMethods holds the usual Remotable-side behavior
  • Any presence.foo() would be looked up in the Presence-side localFunctions, throwing a synchronous error if missing
  • Any presence~.foo() would first be looked up in the Presence-side localFunctions, performing an eventual-send to that method, and if missing it would be sent back to the Remotable, where it invokes something from remoteMethods or throws a no-such-method error

@warner
Copy link
Member Author

warner commented Feb 2, 2021

@warner, I just talked to @erights, and we agreed that we probably don't need to add the amountMath methods to brands before Beta. Instead, with just the mathKind property on the brand presence, we can refactor amountMath to be directly importable and have it dispatch to the correct mathHelpers under the hood.

Ok, cool, if I understand that correctly, then it might be sufficient to do the Remotable(iface, properties, methods) API listed above, where basically everything in properties (which must be pass-by-copy, no methods or functions) gets serialized and glued onto the Presence?

@katelynsills
Copy link
Contributor

katelynsills commented Feb 2, 2021

it might be sufficient to do the Remotable(iface, properties, methods) API listed above, where basically everything in properties (which must be pass-by-copy, no methods or functions) gets serialized and glued onto the Presence?

Yes, I think that's right! props would be { mathKind: 'nat'}, { mathKind: 'set'}, or { mathKind: 'strSet'}.

@warner warner self-assigned this Feb 2, 2021
@warner
Copy link
Member Author

warner commented Feb 2, 2021

Ok, lemme write down the options as I see them for this smaller feature:

Sender-side API choices

1: auxdata is included as non-Function properties on the hardened pass-by-reference object. Currently we treat all-propertie-are-functions objects as pass-by-reference, all-properties-are-non-functions as pass-by-data, reject mixed objects, and treat Far/Remotable markers as declaring an intention to be pass-by-reference (but such objects must still obey the all-function rule)

const rem = harden({
  mathType: 'nat',
  foo() { return 'oof'; },
});
bob~.hello(rem);

In this option, the presence of any Function property makes the object pass-by-reference. The lack of any Function property makes it pass-by-copy. Declarations of Far/Remotable work as before.

2: auxdata is added only in a Remotable() declaration

const rem = Remotable('interfacename', { mathType: 'nat' }, { foo() { return 'oof'; } });

If we choose this API option, the implementation probably needs to stash this auxdata on the Remotable in an unenumerated tagged-Symbol-named property where the liveslots copy of marshal can find it. If it appears as normal non-Function properties, we don't need anything special.

Receiver-side API options

3: auxdata appears as non-Function properties on each Presence object

const bob = harden({
 hello(pres) {
    console.log(pres.mathType); // 'nat'
    pres~.foo(); // returns Promise for 'oof'
  },
});

4: auxdata is retrieved with a special marshal API

import { getAuxData } from '@agoric/marshal';
const bob = harden({
 hello(pres) {
    console.log(getAuxData(pres, 'mathType'); // 'nat'
    pres~.foo(); // returns Promise for 'oof'
  },
});

If we go this way, we'll probably stash the auxdata in a special unenumerated tagged-Symbol-named property on the Presence where marshal can find it.

@warner
Copy link
Member Author

warner commented Feb 2, 2021

Implementation options

5: syscall.send and syscall.resolve acquire an extra argument that contains the auxdata for everything in their arguments

6: somehow include the auxdata as a third entry in capdata (body, slots, and a new auxdata)

7: add a new syscall for vats to deliver auxdata to the kernel

I'm inclined to go with 5. If auxdata can include other Presences, then the sender's graph traversal must also follow the Presence->auxdata->Presence edges, and the auxdata might include an arbitrary number of additional Presences beyond those in the normal args data. This looks a lot like @FUDCo 's work on sending capdata that includes known-to-be-resolved Promises, where (if this happens during a syscall.resolve) we must include all those other Promises in the same syscall (so all their identifiers can be retired at the end of the syscall).

When we do this for Promises, Chip wrote a helper function (in liveslots) which starts from a Promise, serializes everything reachable (via known resolutions) from that point, and returns a full list of promiseIDs and the capdata they are resolved to. I think we might be able to augment this helper function to know about auxdata as well. We serialize the main object graph, and wind up with two side-lists: incidentally resolved promises (promiseID->resolution), and necessary auxdata (objectIDs->aux capdata).

@warner
Copy link
Member Author

warner commented Feb 2, 2021

In my implementation plan, liveslots will need a way to ask marshal for the auxdata for a given Remotable. This could either be module-wide, or another function next to { serialize, unserialize } for each marshal instance. If we store auxdata in a special property of the Remotable (as I think we must, to allow the marshal that liveslots uses to be different than the one imported by the user-level vat code which called Remotable()), then either should work: this new getAuxDataFor(remotable) has no authority, it just knows the magic property symbol convention.

If we present API option 1 (auxdata is provided as normal properties on an otherwise pass-by-reference object), then marshal needs to take care to not serialize those properties as usual: they should only appear in the getAuxDataFor results:

const rem = harden({
  mathType: 'nat',
  foo() { return 'oof'; },
});
const capdata = serialize(rem);
assert.deepEqual(capdata, { body: JSON.stringify({QCLASS: 'slot', index: 0}}, slots: 'o+0' });
assert.deepEqual(getAuxDataFor(rem), { mathType: 'nat' });

If we use API option 2 (special arguments to Remotable()), it's less likely / more obvious that serialize will not include the data properties. The getAuxDataFor behavior would be identical.

@warner
Copy link
Member Author

warner commented Feb 2, 2021

At lunch @dtribble argued for only sending the auxdata when it is known to be necessary. For the vat-to-kernel direction, this means when liveslots allocates an export-side (o+NN) vat-object-id for the first time. It also means the two sides must correctly coordinate the de-allocation of these IDs (which is part of the GC/revocable-objects design).

For the kernel-to-vat direction, it means when the kernel first adds the kref into the clist and allocates the import-side (o-NN) vref.

For the comms protocol, it's when the remote-object-ref (rref) is allocated in the per-remote clist. And the deallocation protocol is all the more important, because this is the one protocol where messages can cross on the wire.

@dtribble
Copy link
Member

dtribble commented Feb 2, 2021

The main point is that there are many more references to the object that have properties than there are objects. For example, every Amount passed will point at it's Brand. There could be billions of Amounts passed for mere hundreds of brands because the usages are a different cardinality. Therefore we should minimize the cost of reference is possible.

Since we should architecturally eliminate any race between the kernel and the vats that it is managing, this should be relatively straightforward in all cases. Otherwise it's non-deterministic what references the kernel has, and we just went through a lot of design work to address that issue. So e.g.,:

  1. exporter exports a new reference, with data props
  2. importer imports with data props
  3. exporter re-exports and already-exported reference, without data props
  4. all importers of a reference drop the reference
  5. kernel tells exporter that the reference is dropped
  6. exporter tells the kernel the reference is no longer exported
  7. kernel drops the reference info, including the property data

The "export tells kernel the reference is no longer exported" SHOULD be (or at least could be) in the form of a single ACK for all the dropped references from the kernel that have not been reused.

@warner
Copy link
Member Author

warner commented Feb 3, 2021

I just spoke with @erights , he said API options 1+3 are ok (mixing Function and data/non-Function properties on both Remotables and Presences). The counterargument is that this makes it harder to introduce a "bare Functions are pass-by-reference" feature in the future, because then how do we distinguish between an all-methods pass-by-reference Remotable and a pass-by-copy record that happens to contain a bunch of individually-referenceable pass-by-reference bare functions?

The answer may be straightforward: we require Far() or Remotable() to mark an object as pass-by-reference, so Far({foo(){..}}) is a pass-by-reference with a method foo, whereas harden({foo(){..}}) is a pass-by-copy record with a property foo whose value is a pass-by-reference bare function. I think that's where #2018 is headed.

Implementation-wise, he's tentatively in support of the registered-symbol property approach, but we need @michaelfig to weigh in. We don't currently have a way to let liveslots and the user-level vat code get the same instance of the marshal module (and thus share WeakMaps defined at the top level of marshal.js) unless/until we get SES and Endo and a new module-loader into play, which depends upon a lot of work by @kriskowal and @dckc that isn't going to happen this month. In the long run he'd really like to share a WeakMap, and we think @michaelfig has plans for marshal/HandledPromise which depend upon it (and the two of them spent a lot of time working on the code that eventually turned into that shared WeakMap, so it's there for a reason, and we need to keep that reason in mind if we switch to a different approach).

@erights also said that they came up with a workaround in the Zoe meeting, such that we may not need to implement auxdata right away, so maybe we can put some more design work into it first. I'm going to prioritize #2018 over auxdata as a result.

@warner
Copy link
Member Author

warner commented Feb 5, 2021

In talking with @erights today about how pass-by-reference objects with non-trivial prototypes should be treated, we speculated that it might be interesting to allow auxdata to include the prototype, as well as properties. If a pass-by-reference object had a pass-by-copy prototype, what would that mean? Or, if we find a way to add local behavior as auxdata, we send a pass-by-ref object whose prototype was another pass-by-ref object, then the local behavior on the two received Presences would obey the usual inheritance rules. And if we also incorporated local state into the mix, things could get really interesting.

@warner
Copy link
Member Author

warner commented Feb 10, 2021

In our meeting today, @erights mentioned that he's got plans for auxdata that will require it to include Promises. I've been hoping to avoid Promises in auxdata, because we retire the Promise's vpid when we resolve it, which means the auxdata will get serialized in different ways at different times, making it harder to be sure a vat isn't equivocating about the auxdata contents.

We speculated that we might serialize these Promises (ones referenced by auxdata) differently, with a marker that says "don't retire this vpid automatically upon resolution", and then keep the same vpid for it until the Remotable itself goes away, releasing the auxdata and releasing the vpid. But I don't think we have any good way to discover that the Promise might be reachable by some auxdata ahead of time. Instead, we need some sort of refcount on the vpid, for which we count one reference for unresolved promises (the kernel needs to coordinate resolutions and pipelined messages on that vpid, but the need for that goes away once it gets resolved). We count another reference for each auxdata that points to it, if that auxdata has been sent to the kernel, and if the object which owns that auxdata is still alive. Once the auxdata goes away, the kernel no longer needs to know what the old vpid referred to, and it can be released.

@warner
Copy link
Member Author

warner commented Mar 24, 2021

Does auxdata+GC enable nondeterminism?

How will auxdata and GC-able Presences interact? Vats are not supposed to be able to sense GC actions. Could the presence of auxdata be used as a source of nondeterminism?

Our general position is that pass-by-copy data lacks identity: userspace should not be surprised if two independent deliveries of equivalent data appear as distinct JavaScript objects. We say "don't depend upon this", but we cannot prevent userspace from trying to sense it anyways. In practice, we serialize message arguments and promise resolutions in a batch, so alice~.foo(data1, data1) will see two references to the same object, while alice~.foo(data1); alice~.foo(data1) will get two distinct objects. This can be sensed by userspace, but is a property of how liveslots performs eventual-send serialization, and is thus a deterministic function of the version of liveslots supporting that vat. This is not a cause for concern.

Pass-by-reference values do have identity, and userspace correctly depends upon this being reflected by JS object identity. Liveslots is responsible for maintaining this equivalence: to the extent that userspace can tell, any time a given vref (like o-12) appears, userspace must see the same JS Presence Object.

Since we also want to be able to free up unused objects, liveslots cannot afford to keep the original Presence around forever. So we must lean on the "to the extent that userspace can tell" condition. Liveslots is allowed to drop the Presence as long as userspace can never tell the difference.

In the current (#2615) GC plan, multiple deliveries that all reference the same imported object ID will give userspace the same Presence Object until+unless userspace drops all its references to that Presence. Once userspace does that (transitioning from the REACHABLE state to the UNREACHABLE state, in #2615 jargon), liveslots is allowed to deliver a different Object on the next referencing message, but is not obligated to do so. In fact, liveslots will keep returning the old Object until the vref moves to the COLLECTED state, because liveslots cannot sense the difference between REACHABLE and UNREACHABLE.

  • REACHABLE: userspace has a strong reference (and userspace does not get WeakRef, so it has no weak references)
  • UNREACHABLE: userspace has no strong references, but the engine has not yet collected the object, so liveslots' WeakRef remails live
  • COLLECTED: the engine has collected the object, liveslots' WeakRef is dead, a finalizer callback is queued
  • FINALIZED: the callback has run, liveslots is holding the vref in the deadSet, the kernel has not yet been notified
  • UNKNOWN: liveslots did a syscall.dropImports to notify the kernel, and has removed the vref from the deadSet

If a message arrives that re-introduces a vref while it is in the UNREACHABLE state, userspace will get the same Object as before. If the vref is in COLLECTED or FINALIZED or UNKNOWN, it will receive a new Presence.

Userspace should not be able to tell the difference, because by definition it has dropped all strong references to the original Presence in all four states, so it has nothing to compare the maybe-new Presence against.

But, if we add auxdata to the Presences, then userspace can hold onto one of the auxdata properties, without causing the Presence to stick around. Then, when a message arrives, they do an Object.is comparison of the new Presence's auxdata and the one they stashed. If it's still the same object, userspace knows the new Presence is the same as the old one. If they differ, userspace can deduce that liveslots must have re-unserialized the auxdata CapData into a fresh copy, and it would only do that if it had to build a new Presence object.

That gives userspace the ability to distinguish a historical transition from UNREACHABLE to COLLECTED, and that transition depends upon the engine's GC schedule. We don't want userspace to be able to sense this, even if we're forcing GC at the end of each crank, because GC might also happen in the middle of a crank, and not very predictably.

I don't have a good solution for this yet. The ideas that come to mind:

  • Never release a Presence that contains auxdata. Eww.
  • Make the auxdata properties be getters, and return a freshly unserialized value each time.
    • I don't think this would work, because userspace could use Object.getOwnPropertyDescriptor to fetch the get() function, and compare that for object identity
  • Make the Presence out of a Proxy whose property lookups return fresh values each time, without exposing an identity-betraying getter function. This feels like the most promising solution.
  • Add some sort of hidden property in all auxdata properties (recursively) that creates a strong reference to the Presence, so any attempt by userspace to stash something would also keep the Presence alive, preventing the transition to UNREACHABLE.
  • Remember all auxdata properties (recursively) via a WeakMap, and find some way to reattach the same ones if userspace has kept them alive
  • Remember all auxdata properties in a big list of WeakRefs, all associated with the vref. Each time we make a new Presence, check the table and poll all the WeakRefs. If any of them are still alive.. well, we know userspace might be holding on to it to do a comparison. Not sure how to use that data. Also we'd need a way to clear out that table if/when userspace has dropped all the auxdata too.

Boy it'd be nice if JS had a way to create a Selfless data object, or to allow objects to refuse to be compared by identity somehow.

@erights do you have any clever ideas here?

@dckc
Copy link
Member

dckc commented Mar 24, 2021

...

  • Make the Presence out of a Proxy whose property lookups return fresh values each time, without exposing an identity-betraying getter function. This feels like the most promising solution.

Seems pretty reasonable. I gather defensive copies are pretty common with pervasive mutability and iffy equality semantics (e.g. Java beans stuff)

Boy it'd be nice if JS had a way to create a Selfless data object, or to allow objects to refuse to be compared by identity somehow.

yup.

warner added a commit that referenced this issue Jun 1, 2021
The kernelKeeper `addKernelObject` method was updated to accept an `id=`
override, to simplify some upcoming unit tests. `deleteKernelObject` was
added, which isn't called yet but the upcoming GC changes will invoke it when
GC allows a kernel object to be deleted. It's also a placeholder for #2069
auxdata to be deleted.
warner added a commit that referenced this issue Jun 1, 2021
The kernelKeeper `addKernelObject` method was updated to accept an `id=`
override, to simplify some upcoming unit tests. `deleteKernelObject` was
added, which isn't called yet but the upcoming GC changes will invoke it when
GC allows a kernel object to be deleted. It's also a placeholder for #2069
auxdata to be deleted.
warner added a commit that referenced this issue Jun 3, 2021
The kernelKeeper `addKernelObject` method was updated to accept an `id=`
override, to simplify some upcoming unit tests. `deleteKernelObject` was
added, which isn't called yet but the upcoming GC changes will invoke it when
GC allows a kernel object to be deleted. It's also a placeholder for #2069
auxdata to be deleted.
@erights
Copy link
Member

erights commented Aug 5, 2022

@erights do you have any clever ideas here?

I do. When I can take the time to do it, I need to write it up. So I'm adding myself to the assignees for this issue.

@erights
Copy link
Member

erights commented Oct 8, 2022

See #6355

@erights erights linked a pull request Oct 8, 2022 that will close this issue
warner added a commit that referenced this issue Nov 17, 2022
When krefOf() is called as part of kmarshal.serialize, marshal will
only give it things that are 'remotable' (Promises and the Far objects
created by kslot()).  When krefOf() is called by kernel code (as part
of extractSingleSlot() or the vat-comms equivalent), it ought to throw
if 'obj' is not one of the Far objects created by our kslot().

This also changes extractSingleSlot() to be just as precise as the old
implementation, to be safe against future changes to krefOf() or the
marshalling format (e.g. #2069 auxdata adding additional properties).
FUDCo pushed a commit that referenced this issue Nov 28, 2022
When krefOf() is called as part of kmarshal.serialize, marshal will
only give it things that are 'remotable' (Promises and the Far objects
created by kslot()).  When krefOf() is called by kernel code (as part
of extractSingleSlot() or the vat-comms equivalent), it ought to throw
if 'obj' is not one of the Far objects created by our kslot().

This also changes extractSingleSlot() to be just as precise as the old
implementation, to be safe against future changes to krefOf() or the
marshalling format (e.g. #2069 auxdata adding additional properties).
FUDCo pushed a commit that referenced this issue Nov 28, 2022
When krefOf() is called as part of kmarshal.serialize, marshal will
only give it things that are 'remotable' (Promises and the Far objects
created by kslot()).  When krefOf() is called by kernel code (as part
of extractSingleSlot() or the vat-comms equivalent), it ought to throw
if 'obj' is not one of the Far objects created by our kslot().

This also changes extractSingleSlot() to be just as precise as the old
implementation, to be safe against future changes to krefOf() or the
marshalling format (e.g. #2069 auxdata adding additional properties).
FUDCo pushed a commit that referenced this issue Nov 29, 2022
When krefOf() is called as part of kmarshal.serialize, marshal will
only give it things that are 'remotable' (Promises and the Far objects
created by kslot()).  When krefOf() is called by kernel code (as part
of extractSingleSlot() or the vat-comms equivalent), it ought to throw
if 'obj' is not one of the Far objects created by our kslot().

This also changes extractSingleSlot() to be just as precise as the old
implementation, to be safe against future changes to krefOf() or the
marshalling format (e.g. #2069 auxdata adding additional properties).
@warner
Copy link
Member Author

warner commented Jun 8, 2023

@erights and I talked through this some more. I think there are a couple of fundamental choices that a system has to make.

Object Identifiers: Absolute, or Relative?

When vats hear about an object identifier, do all vats get the same string, or are they different for each vat? SwingSet uses relative IDs: the vref is scoped to the individual vat, and the kref is scoped to the kernel/machine (two different kernels will re-use ko12 for different objects). Every boundary translates the refs through a c-list. I think E mostly used the same scheme (the integers in the Four Tables), at least outside of SturdyRefs.

Absolute identifiers would present the same string to all clients. This either requires hierarchical counter-like IDs (machineID:vatID:objectID), or entirely random ones, since each must remain unique no matter where they might meet. This is how Foolscap IDs work (every object gets a unique random ID: tubID:objID). I believe E's SturdyRef URLs do the same (which is where I got the inspiration), however only a subset of (explicitly-registered) objects receive SturdyRefs, and the comms system immediately exchanges the unique URL for a c-list entry.

Counter-based object IDs reveal information about how many objects have been allocated, and to prevent this (while still preserving entropy-based uniqueness), the allocator would need to use something like hash(vatSecret + objectCounter) to generate the IDs, or perhaps encrypt(vatSecret, counter) if it were somehow cheaper to decrypt IDs into a counter-indexed table than to just track the whole thing. If we also want to hide the number of vats created, then both of :vatID:objectID would need to be (large) hashes, like 256 bits each. We came up with some applicable tricks for Tahoe-LAFS immutable files, and if you don't need an encryption key out of it, 256 bits would probably be enough to both uniquely identify the object, and strongly hash the auxdata (nonce = random(); objID=hash(nonce+auxdata), then the first time you tell someone about the object, you must provide both nonce and auxdata so they rehash and compare against the objID you're using).

Machine IDs: Stable, or Evolving?

In our standard Granovetter diagram, the initial object references are from A->B and from A->C. At the comms level, what that really means is that both B and C have a recognition predicate for A: when a message appears from the internet, claiming to be from A, e.g. C can apply this predicate and decide that the message contents are worthy of accessing resources previously granted to A (e.g. the contents of an A->C c-list).

When A does E(bob).introduce(carol), what really happens on the C side is that C is introduced to B: machine C acquires a recognition predicate for B, so that when bob exercises his new-found access to carol, his messages (created by B) are accepted by C.

That means A must be able to name B in a message to C, and C must wind up with a recognition predicate for B.

In a "public-key style" system (e.g. Foolscap, E's VatTP, IPFS's P2P protocol, I think Tendermint/CometBFT's P2P protocol), each node is identified by a (single) public key: either a public verifying key (where plaintext messages are signed by the corresponding private signing key), or the public half of a DH keypair (which enables encryption as well as authentication). This public key string is then suitable for use as an identifier, and C can build a recognition predicate from it directly (in practice, there's usually a sequence number on each message, so the predicate is somewhat stateful, but the seqnum is unique to each pair of correspondents, so the B->A seqnum is unrelated to the C->A seqnum, and A doesn't need to include it when telling C about B).

That means the recognition predicate is "stable": it's the same for each recipient, it does not change over time, and it can be used as a fixed identifier for the machine.

Note that the machine ID doesn't have to be exactly the public key: it could be an offline key which is only used to delegate authority to a series of online keys (e.g. "certificate signing key"), or a hash of the key material (where the actual key is fetched at runtime and compared against the hash), or the hash of a stateless program which is retrieved and executed each time a message must be verified. What matters is that it doesn't change over time.

In contrast, a proof-of-stake chain uses a recognition predicate that must know the current validator set, which changes over time (the "light client state" is basically a copy of a recent block, which includes the full list of validator keys and their respective voting power). Worse yet, the economics of slashing and unbonding periods mean that the disincentive against equivocation fades over time: if you don't see messages frequently enough (or if you aren't in a position to report evidence of equivocation fast enough to get the miscreant node slashed), you can lose the ability to safely recognize messages from that sender, even though you used to have that ability.

A can tell C how it (A itself) (currently) recognizes messages from B, and hopefully that will remain valid for long enough to be useful. But when A tells C about B again next month, the predicate will be different.

That means there is no stable/hashable string which can be used to reliably identify B over time. There is generally a "socially unique" string, the chainID, which is randomly generated at chain startup (for cosmos this is the hash of the genesis block, so the initial state of the chain). But there's no way to confirm that a given light client state (aka recent block, aka recognition predicate) matches this ID.

Which means that a malicious A might lie to C about B: present the socially-accepted chainID, but with a bogus light client state, with an intention to disrupt C's ability to hear from B in the future. In the immediate sense, this is entirely within A's rights: until C hears about B from someone else, C cannot distinguish the "real" B from some figment of A's imagination. But, if we imagine that C has a table of remotes, and the key column has B's chainID (as reported by A), and the value column has the current light client state (as initialized by A), and later we receive an introduction from D that also claims to talk about B (same chainID, newer recognition predicate), then A's malice might prevent us from ever recording a "good" value for the predicate.

(it always comes down to the Grant Matcher Problem)

The Cosmos IBC protocol deals with this by requiring special approval (staker vote) for the creation of a channel, which establishes the initial mapping from chainID to light client state. If the predicate ever becomes stale (we fall out of the unbonding window without state updates), the channel freezes until another staker vote delivers a fresh state/block. But normally we get new messages on a regular basis (i.e. at least once a week), which updates the predicate, hopefully keeping it from getting stale.

Note that even for a traditional pubkey system, something as common as a key rotation policy (once a week, key N signs key N+1 and stops using key N-1) means we lose the stable machine ID, and have to fall back to the TOFU/specially-verified relationship between machine ID and recognition predicate.

Message Ordering / Delays, Sync-vs-Async Auxdata

The final set of properties involve how promptly the auxdata will be available to the message recipient.

Ideally, when Alice sends E(bob).introduce(carol):

  • 1: the introduce message is delivered to bob without waiting for vat C to respond
  • 2: bob can read auxdata off of his carol reference with simple carol.dataname property lookups, and gets a synchronous answer

However, if machine B needs to fetch the auxdata from C, instead of relying upon information supplied by A, then we need one of:

  • 1: machine B must delay delivery of introduce until after it hears back from C
  • 2: introduce is called immediately, but the carol argument is actually a Promise for carol, not a Presence, and doesn't resolve until after B hears back from C
  • 3: carol is a Presence, but carol.dataname is a Promise, which doesn't resolve until B hears back from C

The first option causes message ordering problems (perhaps solvable by retreating from E-Order to FIFO-order, but might allow C to interfere with the ordering of unrelated A->B messages). The second changes the signature of introduce in awkward ways (which wasn't such an issue in E, where Promises sort of magically transformed into settled values, but doesn't work for us in JavaScript/SwingSet, where Promises and Presences are distinct things). The third makes auxdata a lot less convenient to use.

Can auxdata be verified?

Now, given those properties of a comms system, under what circumstances can auxdata be verified?

The auxilliary data can either come directly from the owner of the object in question, or it can come from an introducer. The only way to make it synchronously available is to come from the introducer.

If it comes from the introducer, we want to verify it. The only way to independently verify the auxdata (and ensure that it is immutable / constant-over-time, and that everyone sees the same data) is to hash it and embed the hash into the object identifier.

For auxdata that contains object references, the only way to get a constant hash is for the object identifiers to be constant. That requires absolute identifiers (not relative: if B hashes o-12 then it won't match the original o+4 that C created initially). It also requires those identifiers to contain a stable machine ID (if B hashes "current recognition predicate", that won't match the "old recognition predicate" that C used initially).

Chains don't have stable recognition predicates, so the best we can do is a socially-unique name and a manual step to establish the mapping,

So we would basically need to:

  • create the kernel with a machineID (the chainID, or a pubkey for solo machines)
  • create a unique-yet-unrevealing vatID for each new vat
  • create a unique-yet-unrevealing object ID for each new object
    • the kernel must apply the hashing/hierarchical-counter/whatever rules to enforce the complete objectID being one that the vat could legitimately create: no forging of other vat's objectIDs
    • the comms layet must do the same for incoming object IDs: no forging of other kernel's IDs
  • the liveslots layer knows machineID:vatID:objectID for each object, and uses it as the "absref" (absolute reference/slot ID, instead of a vref/kref)
  • replace the kernel-vat c-lists with an "export set" and "import set": one column, not two, holding only absrefs
    • vats can add to the export set as a side-effect of sending messages/resolutions with new absrefs that are scoped to that vat
    • the kernel adds to the import set as a side-effect of sending messages/notifies into the vat with new absrefs that are not scoped to that vat
  • make the kernel object table keyed by absref
  • change comms to use only absrefs, instead of lrefs and rrefs and kfrefs
  • extend the "absref" to include a hash of auxdata (maybe with an empty string when there is no auxdata, to save space in the common case)
  • require a manual step (controller.addRemote? something in vat-vattp or comms?) to introduce a new non-stable-machine-ID remote peer to a kernel, or to refresh a stale predicate. Needed for chains, not needed when the peer is a solo.
    • note: I made vat-vattp a separate vat specifically to own things like this (also message ordering), so comms doesn't need to know about how a remote peer is defined or validated

@FUDCo
Copy link
Contributor

FUDCo commented Jun 8, 2023

Limiting auxdata to data objects that don't contain object references (aka "only bits") seems like it would avoid many of the complications raised above while still providing actual value for a number of use cases.

@mhofman
Copy link
Member

mhofman commented Jun 14, 2023

Ideally, when Alice sends E(bob).introduce(carol):

  • 1: the introduce message is delivered to bob without waiting for vat C to respond

This seems predicated on the assumption that whoever introduced Carol to Alice did not include the auxdata along the presence.

Why can't we say that in a point to point exchange between 2 parties, if one is introducing a new presence, that introduction includes the auxdata. In the exchange above, Alice would include the auxdata of Carol when introducing Carol to Bob. That way Bob does not need to contact vat C to get the data.

Can auxdata be verified?

I have to admit I don't fully (yet) grok recognition predicates in the context of blockchains, or even how multiple parties handle authentication in non blockchain contexts. My understanding is that it's roughly built around wrapped signatures / recognition predicates, with possible shortening of auth chains when a direct connection is established between parties.

That said I guess I don't really understand what is the fundamental difference between a single presence as we have today and potentially other presences in auxdata. If the receiver of a presence is able to validate/trust the primary presence, why can't it similarly validate/trust presences in auxdata? More specifically, if the auxdata is CapData encoded, it would be composed of a stable body (which can be hashed for verification), and slots which can be rewritten when going through layers the same way the primary presence would be.

At the end of the day, I consider our current presence sharing as a degenerate case of a more generic presence sharing mechanism which represents each "presence" as more complex passable data. In that model, what we currently have is the equivalent of a "body" solely composed of a single slot, and the primary presence in that slot.

@dckc
Copy link
Member

dckc commented Jan 4, 2024

to aid in finding this via search: auxiliary has 1 l, not 2.

@turadg turadg changed the title add (immutable) auxilliary data to Presences? add (immutable) auxiliary data to Presences? Jan 4, 2024
warner added a commit that referenced this issue Aug 11, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
warner added a commit that referenced this issue Aug 11, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
warner added a commit that referenced this issue Aug 11, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
warner added a commit that referenced this issue Aug 12, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
mergify bot pushed a commit that referenced this issue Aug 14, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
kriskowal pushed a commit that referenced this issue Aug 27, 2024
the "TODO: decref #2069 auxdata" comment was removed, because that will
be the responsibility of deleteKernelObject()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request marshal package: marshal needs-design SwingSet package: SwingSet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants