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

contract upgrade proposals fragile due to promiseSpace reset strange behavior #7709

Closed
dckc opened this issue May 12, 2023 · 3 comments · Fixed by #7710
Closed

contract upgrade proposals fragile due to promiseSpace reset strange behavior #7709

dckc opened this issue May 12, 2023 · 3 comments · Fixed by #7710
Assignees
Labels
bug Something isn't working vaults_triage DO NOT USE

Comments

@dckc
Copy link
Member

dckc commented May 12, 2023

Describe the bug

while working on upgrade testing (#6099) we ran into strange behavior that causes subtle failures in core eval proposals.

it seems to come down to a bug around the reset() method from makePromiseSpace.

To Reproduce

Steps to reproduce the behavior:

test('resolve after reset', async t => {
  /** @type {MapStore<string, string>} */
  const store = makeScalarBigMapStore('stuff', { durable: true });

  const { consume, produce } = makePromiseSpace({ store });
  {
    const { resolve, reset } = produce.foo;
    // const foo2 = consume.foo;
    reset();
    resolve(1);
    // t.is(await foo2, 1);
    t.is(await consume.foo, 1);
  }

  {
    const { resolve, reset } = produce.foo;
    reset();
    resolve(2);
    t.is(await consume.foo, 2);
  }
});

Expected behavior

test above ^ passes. Perhaps some other design constraints need some thought.

Platform Environment

  • what OS are you using? what version of Node.js?
  • is there anything special/unusual about your platform?
  • what version of the Agoric-SDK are you using? (run git describe --tags --always)

Additional context

Add any other context about the problem here.

Screenshots

If applicable, add screenshots to help explain your problem, especially for UI interactions.

cc @michaelfig @turadg @warner

@dckc dckc added the bug Something isn't working label May 12, 2023
@warner
Copy link
Member

warner commented May 12, 2023

The basic problem is that reset() isn't changing the closed-over state or pk variables. It deletes name from nameToState, so the next time someone reaches through produce or consume (hitting the Proxy's get trap) and calls provideState, they'll get a new value, but if someone has already captured the resolve or reject (i.e. const { name } = produce for a later name.resolve()), they'll have a resolver for the old promise, which isn't what a new const { name } = consume getter will get.

This is triggered by a pattern in @dckc 's init-proposal test, which wasn't in the unit tests:

const { name } = produce;
name.reset();
name.resolve(newThing);

If the .reset and the .resolve happened in two separate functions, then the second produce.name lookup would have triggered a new provideState(), which would have used the right pk.resolve.

To fix this best, I think we should remove the closed-over variables state and pk, and have all the methods work solely with nameToState.get(name) values. But I need to sketch that out and see if it actually makes sense.

@erights
Copy link
Member

erights commented May 12, 2023

Is the abstraction buggy in a way revealed by the test? Or is the test buggy in that it assumes a stronger property of the abstraction than the abstraction is supposed to provide?

@warner
Copy link
Member

warner commented May 12, 2023

I think it's the former. It should be legal to capture both reset() and resolve() at the same time, then call reset() followed by resolve(), if only because that's the simplest thing for a "replace the old value with a new one" behavior function to do. Asking these functions to capture the two values separately would preclude the use of a destructuring assignment like { produce: { name: { reset, resolve } } }, which is how most of our existing behavior functions work (albeit none of them use reset, only this special test).

@erights erights self-assigned this May 12, 2023
warner added a commit that referenced this issue May 12, 2023
Previously, the reset() feature misbehaved in a common use pattern,
where both the `reset` and `resolve` facets were extracted at the same
time. The state was nominally held in `nameToState.get(name)`, but was
sometimes used by closed-over `state` and `pk` variables. Which one
you got depended upon when the Proxy `get` trap was called. If
`resolve` was fetched early and retained across a `reset()` call, then
`resolve()` would resolve the *old* promise. Later, when `consume` was
used, the space would create a new Promise to satisfy the request,
which would never be resolved.

This changes the implementation to strictly keep/use the state in
`nameToState`, and allow all resolve/reject/reset methods to work the
same way no matter when they were retrieved (they close over `name`
but not any state).

closes #7709
warner added a commit that referenced this issue May 12, 2023
Previously, the reset() feature misbehaved in a common use pattern,
where both the `reset` and `resolve` facets were extracted at the same
time. The state was nominally held in `nameToState.get(name)`, but was
sometimes used by closed-over `state` and `pk` variables. Which one
you got depended upon when the Proxy `get` trap was called. If
`resolve` was fetched early and retained across a `reset()` call, then
`resolve()` would resolve the *old* promise. Later, when `consume` was
used, the space would create a new Promise to satisfy the request,
which would never be resolved.

This changes the implementation to strictly keep/use the state in
`nameToState`, and allow all resolve/reject/reset methods to work the
same way no matter when they were retrieved (they close over `name`
but not any state).

closes #7709
@dckc dckc changed the title promiseSpace reset strange behavior core eval proposals fragile due to promiseSpace reset strange behavior May 12, 2023
@dckc dckc changed the title core eval proposals fragile due to promiseSpace reset strange behavior contract upgrade proposals fragile due to promiseSpace reset strange behavior May 12, 2023
dckc pushed a commit that referenced this issue May 12, 2023
Previously, the reset() feature misbehaved in a common use pattern,
where both the `reset` and `resolve` facets were extracted at the same
time. The state was nominally held in `nameToState.get(name)`, but was
sometimes used by closed-over `state` and `pk` variables. Which one
you got depended upon when the Proxy `get` trap was called. If
`resolve` was fetched early and retained across a `reset()` call, then
`resolve()` would resolve the *old* promise. Later, when `consume` was
used, the space would create a new Promise to satisfy the request,
which would never be resolved.

This changes the implementation to strictly keep/use the state in
`nameToState`, and allow all resolve/reject/reset methods to work the
same way no matter when they were retrieved (they close over `name`
but not any state).

closes #7709
@ivanlei ivanlei added the vaults_triage DO NOT USE label May 12, 2023
@ivanlei ivanlei added this to the Vaults Validation milestone May 12, 2023
warner added a commit that referenced this issue May 12, 2023
Previously, the reset() feature misbehaved in a common use pattern,
where both the `reset` and `resolve` facets were extracted at the same
time. The state was nominally held in `nameToState.get(name)`, but was
sometimes used by closed-over `state` and `pk` variables. Which one
you got depended upon when the Proxy `get` trap was called. If
`resolve` was fetched early and retained across a `reset()` call, then
`resolve()` would resolve the *old* promise. Later, when `consume` was
used, the space would create a new Promise to satisfy the request,
which would never be resolved.

This changes the implementation to strictly keep/use the state in
`nameToState`, and allow all resolve/reject/reset methods to work the
same way no matter when they were retrieved (they close over `name`
but not any state).

closes #7709
warner added a commit that referenced this issue May 12, 2023
Previously, the reset() feature misbehaved in a common use pattern,
where both the `reset` and `resolve` facets were extracted at the same
time. The state was nominally held in `nameToState.get(name)`, but was
sometimes used by closed-over `state` and `pk` variables. Which one
you got depended upon when the Proxy `get` trap was called. If
`resolve` was fetched early and retained across a `reset()` call, then
`resolve()` would resolve the *old* promise. Later, when `consume` was
used, the space would create a new Promise to satisfy the request,
which would never be resolved.

This changes the implementation to strictly keep/use the state in
`nameToState`, and allow all resolve/reject/reset methods to work the
same way no matter when they were retrieved (they close over `name`
but not any state).

closes #7709
@warner warner closed this as completed in c191ee1 May 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working vaults_triage DO NOT USE
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants