Cannot create commits on local `inmemory` ceramic daemon

Hey,

I’m unable to test my app on a local ceramic node. When creating a new account, the commit call fails with the following error:

ERROR: Error: Commit rejected by conflict resolution. Rejected commit CID: bagcqcera...

The communication with ceramic seems fine up until this point, and the request data appears well-formed.

What does this error message mean? Where would you suggest I try debugging this?

Can you provide the code you’re using to created the commit? It’s hard to know what’s wrong without seeing it.

I’m using the self.id/web library. I authenticate like so:


const CERAMIC_URL = process.env.CERAMIC_URL || 'http://localhost:7007'

export async function connectIDX(signer: Signer) : Promise<SelfID<ConfigType>> {
  log.trace("IDX: Initiating connection...");

  const authProvider = await createAuthProvider(signer);
  const aliases = await getConfig();
  const self = await SelfID.authenticate<ConfigType>({
    authProvider,
    ceramic: CERAMIC_URL,
    connectNetwork: process.env.THREEID_NETWORK as ConnectNetwork|undefined,
    aliases,
  });

  log.trace(`IDX: connected ${!!self?.id}...`);

  // Attempt link, do not await this.
  createLink(self, authProvider);
  return self;
}

My .env file has the following:

# TODO: ThreeID should run entirely self-hosted
THREEID_NETWORK=local
CERAMIC_URL=http://localhost:7007

Pretty bog-standard, works on testnet and I’m pretty sure it used to work many iterations ago.

The only other thing I can think that might be relevant is our bootstrap process?

import { promises } from "fs";
import { CeramicClient } from '@ceramicnetwork/http-client'
import { ModelManager } from '@glazed/devtools'
import { Ed25519Provider } from 'key-did-provider-ed25519';
import { fromString } from 'uint8arrays';
import { getResolver } from 'key-did-resolver'
import { DID } from 'dids';

const idxFolder = new URL('../src/', import.meta.url);
const schemaPath = new URL(`config.${process.env.CONFIG_NAME}.json`, idxFolder);
const { writeFile, readFile } = promises;

//
// Publish our schema to the node (must happen once per deploy)
async function publish(manager: ModelManager) {
  // Publish the two schemas
  const schema = await readFile(new URL('schemaJWE.json', idxFolder), 'utf8');
  const SchemaJWE = JSON.parse(schema);
  const schemaId =  await manager.createSchema("jwe", SchemaJWE);
  const schemaUrl = manager.getSchemaURL(schemaId);
  if (!schemaUrl) throw new Error("Cannot publish schema: missing URL");

  const privateDetails = await manager.createDefinition("AccountDetails", {
    name: 'AccountDetails',
    description: 'Verified account details',
    schema: schemaUrl,
  })
  return privateDetails;
}

//
// Connect to the Ceramic node
async function connect() {
  // Create and authenticate the DID
  const seed = fromString(process.env.CERAMIC_SEED!, 'base16')
  const did = new DID({
    provider: new Ed25519Provider(seed),
    resolver: getResolver(),
  })
  // Authenticate the Ceramic instance with the provider
  await did.authenticate()

  // The seed must be provided as an environment variable
  const ceramic = new CeramicClient(process.env.CERAMIC_URL)
  await ceramic.setDID(did)
  return ceramic;
}

async function run() {

  // Connect to local node
  const ceramic = await connect();
  // Create a manager for the model
  const manager = new ModelManager({ceramic})

  // prepare schema for publishing
  await publish(manager);

  // Publish and store details
  const model = await manager.deploy();

  // Write details to path
  const config = JSON.stringify(model, null, 2)
  await writeFile(schemaPath, config);

  console.log(`Config written to ${schemaPath}`, config)
  process.exit(0)
}

run().catch(console.error)

This completes, I think correctly, and again - it used to work (I’m running on clay & mainnet).

My daemon starts with the following config:

[2022-08-02T14:23:49.233Z] IMPORTANT: Starting Ceramic Daemon at version 2.4.0 with config:
{
  "anchor": {},
  "http-api": {
    "cors-allowed-origins": [
      ".*"
    ]
  },
  "ipfs": {
    "mode": "bundled"
  },
  "logger": {
    "log-level": 2,
    "log-to-files": false
  },
  "metrics": {
    "metrics-exporter-enabled": false,
    "metrics-port": 9090
  },
  "network": {
    "name": "inmemory"
  },
  "node": {},
  "state-store": {
    "mode": "fs"
  },
  "indexing": {
    "db": "sqlite:///Users/kiwi_/.ceramic/indexing.sqlite",
    "models": []
  }
}
[2022-08-02T14:23:49.273Z] IMPORTANT: Connecting to ceramic network 'inmemory' using pubsub topic '/ceramic/inmemory-3160334000'
[2022-08-02T14:23:49.282Z] IMPORTANT: Connected to anchor service '<inmemory>' with supported anchor chains ['inmemory:12345']
[2022-08-02T14:23:49.342Z] IMPORTANT: Ceramic API running on 0.0.0.0:7007'

The one difference this has to regular work is that I’m built the management app from ‘develop’ branch. (So I can have my own iframe @ localhost:30001), so it might be a bit different to whatever has been published on clay/mainnet.

Related question: if I didn’t have to host this iframe, I wouldn’t. Is there any way to have the clay iframe connect to localhost chain?

Most requests succeed until the call to commit:

<image removed because “you can’t embed media items in a post”>

VM3416 LinkNewDID.tsx:50 ID creation error Error: HTTP request to 'http://localhost:7007/api/v0/commits' failed with status 'Internal Server Error': {"error":"Commit rejected by conflict resolution. Rejected commit CID: bagcqcerafg6t4zu63xxjush6q43sl2itzw2x5tffnghrffnji7cgccm7vv5a "}
    at fetchJson (http-utils.js?ae17:19:1)
    at async Document.applyCommit (document.js?79a0:53:25)
    at async CeramicClient.applyCommit (ceramic-http-client.js?13a1:94:1)
    at async TileDocument.update (tile-document.js?defa:120:1)
    at async DIDDataStore._getOwnIDXDoc (index.js?c115:273:1)
    at async TileProxy.eval (index.js?c115:284:34)
    at async TileProxy.get (proxy.js?b8da:80:1)
    at async DIDDataStore.getIndex (index.js?c115:220:1)
    at async DIDDataStore.getRecordID (index.js?c115:312:1)
    at async DIDDataStore.getRecordDocument (index.js?c115:318:1)
    at async DIDDataStore.getRecord (index.js?c115:324:1)
    at async DIDDataStore.get (index.js?c115:103:1)
    at async ThreeIDX.getAuthMap (three-idx.js?d210:209:1)
    at async Keychain.list (keychain.js?6494:141:1)
    at async ThreeIdProvider.create (threeid-provider.js?c691:110:1)
    at async Manager._initIdentity (manager.js?04a6:102:1)
    at async Manager.createAccount (manager.js?04a6:66:1)

The offending payload:

{
    "streamId": "k2t6wyfsu4pg304irxez0bv057wc0iob05owmcg82pohg3kj1rrv4qoxstm3ld",
    "commit": {
        "jws": {
            "payload": "AXESINJLdnd95ck-dKO2MozLCtbjUL7M8yEU5v7eYlkPkKXo",
            "signatures": [
                {
                    "protected": "eyJraWQiOiJkaWQ6MzpranpsNmN3ZTFqdzE0NjltbHpqNWtrdGgxbjd1MWc3OHVvaHJsY2k3N2Q1Y3V1MXJobjhqN2E4cXFyaHVzM3k_dmVyc2lvbi1pZD0wI1NRb0JNZFZjSGVXaDUzcyIsImFsZyI6IkVTMjU2SyJ9",
                    "signature": "hgBQB_Q4eXdo2DbIUyIW1vXq0wlEilgL5OLG_DxC-pghIEDOMk6bC9XgL_fclz9ilGvW-w_zKW1GEX2Bw1ngSg"
                }
            ],
            "link": "bafyreigsjn3ho7pfze7hji5wgkgmwcww4nil5thteekon7w6mjmq7eff5a"
        },
        "linkedBlock": "pGJpZNgqWCUAAXESIP1roV2+Aj2rEBffm6M6zQ0RPD//wH2NlFA5zW+YMfixZGRhdGGAZHByZXbYKlglAAFxEiD9a6FdvgI9qxAX35ujOs0NETw//8B9jZRQOc1vmDH4sWZoZWFkZXKhZnNjaGVtYXhLY2VyYW1pYzovL2szeTUybDdxYnYxZnJ5am42MnNnZ2poMWxwbjExYzU2cWZvZnptdHkxOTBkNjJod2sxY2FsMWM3cWM1aGU1NG93"
    },
    "opts": {
        "anchor": true,
        "publish": true,
        "sync": 0,
        "throwOnInvalidCommit": true
    }
}

My daemon logs the following:

[2022-08-02T14:37:17.339Z] ERROR: Error: Commit rejected by conflict resolution. Rejected commit CID: bagcqcerafg6t4zu63xxjush6q43sl2itzw2x5tffnghrffnji7cgccm7vv5a

I’ve tried deleting the %USER%/.ceramic folder to delete cache etc…

Any assistance would be very much appreciated

  1. The iframe just handles signing, if you don’t host it yourself & just use the one we provide it’ll still generate accounts on your node as long as you pass in the right details. (I’m spacing on the config but if memory serves there’s a param to pass in the network type.
  2. Is this error occuring on creation or are you trying to update the commit? The conflict resolution error isn’t making a lot of sense atm with what I see in your code. Additionally are you using a schema or are you just creating?

What might be happening is internally we’ve begun focusing more on did-session & on CACAO for our DIDs, it’s possible that an update was missed for the Self.ID SDKs, can you attempt to fuse a key-did instead of one generated through Self.ID (sample code here)

  1. It happens on the first commit even with fresh Ceramic/IPFS combo.

It’s not technically the first commit because we bootstrap our custom definitions first prior to launching our app, but the commit getting rejected here isn’t ours - it happens before we even get to our code (in the SelfID.authenticate call.

  1. It feels like that should be the case, but I couldn’t figure out how last time I took a crack at it. I’ll try again
  1. Mixing networks almost works,
# TODO: ThreeID should run entirely self-hosted
THREEID_NETWORK=testnet-clay
CERAMIC_URL=http://localhost:7007

but on account creation I get

Error: Failed to resolve did:3:kjzl6cwe1jw146vyxh3bf1l325qcb3b4rxnns181whfw1mezrpmgyia2mm2otzm?version-id=0#yFiNsYeCGwn4b16: invalidDid, Error: Failed to properly resolve 3ID, stream kjzl6cwe1jw146vyxh3bf1l325qcb3b4rxnns181whfw1mezrpmgyia2mm2otzm not found in response

Looking through the network connections there is an odd mix of requests to fer example https://ceramic-private-clay.3boxlabs.com/api/v0/multiqueries and http://localhost:7007/api/v0/multiqueries - it seems like it’s loading some of the data from clay (where an account has been previously created) and localhost (which I just flushed clean).

I’ll keep digging, but if you have any suggestions they’d be much appreciated

RE: Using key-did - I’m sorry, I can’t figure out what your asking me to test here? Is it creating a CeramicInstance with authenticated DID to replace the self.id instance? Then query the ceramic daemon to see if we can create some (any) kind of commit? (Similar to the bootstrap code above)

I’m just a little confused because the sample code linked doesn’t appear to show that - I’m actually not sure what it shows. When I try to replicate the sample in my library I can’t pass the Ceramic instance to the KeyResolver, and I’m not sure which version is out of date. If I skip that step tho (similar to bootstrap) I can’t imagine how keyResolver manages to resolve anything.

Hey, is there anything else I can do to figure out what is going wrong here? It would make my testing much easier if I could reset my chain at-will, but I don’t know where to start poking around.

Hey @FrozenKiwi, are you able to load stream ID k2t6wyfsu4pg304irxez0bv057wc0iob05owmcg82pohg3kj1rrv4qoxstm3ld from your local Ceramic daemon? I’m guessing not after clearing the Ceramic state store.

Conflict resolution is a way to unify histories if (for some reason) they diverge. We don’t support multiple writers to a single stream (and don’t plan to), and are still working on a load-balanced version of Ceramic (coming soon :crossed_fingers:t4:).

The latter means that currently, if updates to a stream are applied from two different places (e.g. two Ceramic instances being run behind a load balancer), it is possible that conflict resolution will reject updates from one source. This situation can happen in different ways, e.g. updates being done through a local Ceramic daemon and on Clay nodes at the same time.

Could this be what’s happening?

Hey, @FrozenKiwi , one more question I have is, whether you could connect your local ceramic node to testnet-clay

As of now, we don’t support Self.id with local ‘inmemory’ ceramic nodes.

if updates to a stream are applied from two different places

I don’t see how that is possible. I have a single webpage running, and the network tab only contains one call to commit. The commits originate from a saga, but I checked it’s takeLatest and not takeEach.

I deleted the caches (web & ceramic), and tried again. After the error was thrown I tried reading the stream & found 2 commits in it. I can’t really tell what is in the commits, but they were type 0 and type 2 - so the stream is created and writable, at least until whatever is in ‘commit’ is sent to it. (I have to admit, I find the internals of ceramic a little dense, I think I’m starting to feel the building blocks but honestly, I’m trying to treat it as black-box as possible).

One thought - is it possible that the local in-memory node needs to be bootstrapped? Perhaps it’s missing some built-in stream type/controller? I’m running it as brand-new, fresh, with just my custom stream type explicitly deployed.

whether you could connect your local ceramic node to testnet-clay

Yes, this works. However, I’m curious as to what the difference is between this & just connecting directly to testnet-clay. Does any data from my local commits propagate to the testnet? My testing accounts are not random, and I just want to know that every time I run a session they start fresh & clean.

It’s also nice to have an offline-only environment, and especially to have it insulated from changes in the main trunk ;-).

As of now, we don’t support Self.id with local ‘inmemory’ ceramic nodes.

Fair enough, although it would be nice for the reasons above. I also bet doubly you don’t support it on windows, (I’m bit of a sucker for punishment).

I think I understand the problem. The error you are seeing happens when the ceramic client has out of date state relative to the ceramic node. You mentioned that the stream on the node has 2 commits - one with type 0 and one with type 2. The type 0 commit is the genesis commit - the initial state that the stream is created with. Type 2 commits are anchor commits, representing when the previous commit was anchored on to the blockchain. With nodes running on real networks, anchors happen periodically: once every 30 minutes on mainnet for example. With a node on the inmemory network, however, we don’t actually anchor to a real blockchain, we just anchor into an object in memory. This means the anchors happen near instantaneously. So between when you create and when you update the stream, the stream gets anchored, which creates a new commit in the stream log, which causes your update commit to fail since it conflicts with the anchor commit that the client knows nothing about.

Sorry for the confusion, the inmemory network hasn’t really been used much outside of our internal unit tests, so no one has really hit these types of usability issues with it before. To work around this you could call .sync() on your stream handle between when the stream is created and when it is updated, to make the client pull in the anchor commit into its view of the stream state.

2 Likes