-
Notifications
You must be signed in to change notification settings - Fork 2
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
fix(registration): use unique registration ID #82
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tk-o Hey super effort to advance this on the weekend 💪 Much appreciated! Reviewed and shared some ideas with feedback 😄
// handled by `eth` plugin | ||
labelhash | ||
: // For other plugins, we use the namehash to ensure uniqueness | ||
namehash; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Collisions between labelhash
and namehash
should be quite unlikely, but we should make a stronger 100% guarantee that collisions are impossible. Ex: Return a concatenation of ownedName
, a symbol such as ":" (or whatever you suggest), and namehash
.
Part of the motivation for including ownedName
in this key is to protect against a plugin writing names to their shadow registry outside the scope of names they are expected to manage.
For example, consider the case that the base.eth plugin emits events related to the registration of example.linea.eth or example.eth.
In fact, suggest that we take this opportunity to add some validation checks on both the base.eth and linea.eth plugins to validate that they only ever index names that are subnames of their respective ownedName
.
It's ok for those to be subnames of subnames of subnames, etc.. But ultimately the ownedName
must be the root of all subnames added to the shadow registry associated with those extensions.
If this change introduces too much scope for this existing PR 82, please feel welcome to create a separate issue for us to track this goal. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
personally i think collisions can be safely ignored given that they're keccak hashes. no known collision has ever been discovered and if such an event occurs i don't think anyone would say ensnode is at fault for not handling that case.
re: validation — it seems safe to throw that validation logic in, but i question the need for it, since we do have verified contract source. perhaps that may not be true in the future, if we integrate a malicious registrar? but it doesn't seem like a likely threat at the moment. lmk if i'm off here. regardless should definitely be a separate issue
ps. sadly a concatenation also isn't as trivial because of the Hex
type of the field, it may break some assumptions within ponder if we commit non-hex data, even if it is represented as TEXT in postgres. sticking with label or node is efficient, clear, and causes 0 likely problems, so i think it's a perfect solution.
// Makes a cross-chain unique registration ID for a given owned name. | ||
export const makeRegistrationId = (ownedName: string, labelhash: Hex, namehash: Hex) => | ||
ownedName === "eth" | ||
? // To keep subgraph compatibility, we still use labelhash for registrations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? // To keep subgraph compatibility, we still use labelhash for registrations | |
? // To keep subgraph compatibility, we still use labelhash for RegistrationIds |
@@ -11,6 +11,15 @@ export const makeSubnodeNamehash = (node: Hex, label: Hex) => keccak256(concat([ | |||
// converts uint256-encoded nodes to hex | |||
export const tokenIdToLabel = (tokenId: bigint) => toHex(tokenId, { size: 32 }); | |||
|
|||
// Makes a cross-chain unique registration ID for a given owned name. | |||
export const makeRegistrationId = (ownedName: string, labelhash: Hex, namehash: Hex) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const makeRegistrationId = (ownedName: string, labelhash: Hex, namehash: Hex) => | |
export const makeRegistrationId = (ownedName: string, labelhash: Hex, node: Hex) => |
- "namehash" refers to the function, while "node" returns to the return value of that function. There should be detailed documentation for this param, ex: that this is the node of the name that was registered.
- Please also refine the following:
- Please add detailed documentation for
ownedName
and what this represents. Ex: It is theownedName
of the ENSNode plugin that is processing the registration event. - Please add detailed documentation for
labelhash
and what this represents. For example, it needs to be 100% clear what this is the labelhash of. Ex: Is it the labelhash of the direct subname ofownedName
? What if the registration is more than 1 level of subname beneathownedName
? What iflabelhash
has no child relationship at all withownedName
? For example: Imagine the "eth" plugin where a name such as tko.box is explicitly added to the ENS Registry. Exactly what is labelhash the labelhash of in this case? The same question if the name abc.123.456.tko.box was explicitly added to the ENS Registry under the "eth" plugin. I'm assuming that this is the labelhash of the subname being registered, but that we also need to make it clear that the subname being registered could be any subname at any level within the scope of registrations indexed by the ENSNode plugin associated withownedName
. It should also be noted how the "eth" plugin can theoretically index all possible names given the NameWrapper.
- Please add detailed documentation for
Please also note how a source of some of the complexity here is how this makeRegistrationId
utility function is in a totally generic "subname-helpers.ts" file, which suggests that the logic inside of it should apply universally in all cases. This is a key reason for some of the feedback above.
Alternatively, it could be considered to only put this function in apps/ensnode/src/handlers/Registrar.ts
and NOT export it. It would still need careful documentation, but perhaps a bit less, since as I understand it would only apply to direct subnames of the ownedName
of the associated plugin.
Advice appreciated 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i support this helper living in ids.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i also think the included documentation is mostly acceptable, perhaps some additional context at the top is warranted like:
a Registration record's
id
is historically the labelhash of the label directly under the name the Registrar manages (i.e. for a registration oftest.eth
, a Registration's id islabelhash('test')
). Because the original subgraph implementation didn't consider indexing any Registrars besides the .eth Registrar, this leaves no room in the namespace for Registration records from other Registrars (like the .base.eth and .linea.eth Registrars). To account for this, while preserving backwards compatibility, Registration records created for the .eth Registrar uselabel
asid
and those created by any other Registrar usenode
(i.e. namehash(test.base.eth
) to avoid collisions that would otherwise occur.
i think the inline comments are excellent, though perhaps unnecessary if the larger comment on the helper looks like my suggestion above
@@ -11,6 +11,15 @@ export const makeSubnodeNamehash = (node: Hex, label: Hex) => keccak256(concat([ | |||
// converts uint256-encoded nodes to hex | |||
export const tokenIdToLabel = (tokenId: bigint) => toHex(tokenId, { size: 32 }); | |||
|
|||
// Makes a cross-chain unique registration ID for a given owned name. | |||
export const makeRegistrationId = (ownedName: string, labelhash: Hex, namehash: Hex) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i support this helper living in ids.ts
// handled by `eth` plugin | ||
labelhash | ||
: // For other plugins, we use the namehash to ensure uniqueness | ||
namehash; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
personally i think collisions can be safely ignored given that they're keccak hashes. no known collision has ever been discovered and if such an event occurs i don't think anyone would say ensnode is at fault for not handling that case.
re: validation — it seems safe to throw that validation logic in, but i question the need for it, since we do have verified contract source. perhaps that may not be true in the future, if we integrate a malicious registrar? but it doesn't seem like a likely threat at the moment. lmk if i'm off here. regardless should definitely be a separate issue
ps. sadly a concatenation also isn't as trivial because of the Hex
type of the field, it may break some assumptions within ponder if we commit non-hex data, even if it is represented as TEXT in postgres. sticking with label or node is efficient, clear, and causes 0 likely problems, so i think it's a perfect solution.
@@ -11,6 +11,15 @@ export const makeSubnodeNamehash = (node: Hex, label: Hex) => keccak256(concat([ | |||
// converts uint256-encoded nodes to hex | |||
export const tokenIdToLabel = (tokenId: bigint) => toHex(tokenId, { size: 32 }); | |||
|
|||
// Makes a cross-chain unique registration ID for a given owned name. | |||
export const makeRegistrationId = (ownedName: string, labelhash: Hex, namehash: Hex) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i also think the included documentation is mostly acceptable, perhaps some additional context at the top is warranted like:
a Registration record's
id
is historically the labelhash of the label directly under the name the Registrar manages (i.e. for a registration oftest.eth
, a Registration's id islabelhash('test')
). Because the original subgraph implementation didn't consider indexing any Registrars besides the .eth Registrar, this leaves no room in the namespace for Registration records from other Registrars (like the .base.eth and .linea.eth Registrars). To account for this, while preserving backwards compatibility, Registration records created for the .eth Registrar uselabel
asid
and those created by any other Registrar usenode
(i.e. namehash(test.base.eth
) to avoid collisions that would otherwise occur.
i think the inline comments are excellent, though perhaps unnecessary if the larger comment on the helper looks like my suggestion above
Allows naming arguments more precisely with primitive types and doc comments attached to them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
apps/ensnode/src/lib/primitives.ts
Outdated
@@ -0,0 +1,31 @@ | |||
import type { Hex } from "viem"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is nice actually, good idea! a nitpick on filename: maybe ens-types.ts
? or i've just used just types.ts
in the past but maybe too generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've had it as types
, but primitives
also worked in my mind. However, let's stick to common naming standards for TS, and go with just types
for the time being.
rename `primitives.ts` to `types.ts`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tk-o Hey nice work here. Reviewed and shared feedback 👍
apps/ensnode/src/lib/types.ts
Outdated
* ``` | ||
* @link https://docs.ens.domains/ensip/1#namehash-algorithm | ||
*/ | ||
export type Node = Hex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are great! Suggest moving the definition of things that will likely be shared across different layers of our project, such as Node
and Labelhash
into the ensnode-utils
package. Open to doing that only when required though. Appreciate your advice.
apps/ensnode/src/lib/ids.ts
Outdated
@@ -10,3 +11,24 @@ export const makeEventId = (blockNumber: bigint, logIndex: number, transferIndex | |||
[blockNumber.toString(), logIndex.toString(), transferIndex?.toString()] | |||
.filter(Boolean) | |||
.join("-"); | |||
|
|||
/** | |||
* Makes a cross-chain unique registration ID. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Makes a cross-chain unique registration ID. | |
* Makes a cross-registrar unique registration ID. |
The key to the logic here is making it so registration IDs will be unique across Registrars. It wouldn't matter if those Registrars are on the same chain or not.
apps/ensnode/test/ids.spec.ts
Outdated
@@ -17,4 +17,18 @@ describe("ids", () => { | |||
expect(makeEventId(123n, 456)).toEqual("123-456"); | |||
}); | |||
}); | |||
|
|||
describe("makeRegistrationId", () => { | |||
it("should use label when ownedName is exactly `eth` to ensure subgraph compatibility", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it("should use label when ownedName is exactly `eth` to ensure subgraph compatibility", () => { | |
it("should use labelhash when ownedName is `eth` to ensure subgraph compatibility", () => { |
apps/ensnode/test/ids.spec.ts
Outdated
); | ||
}); | ||
|
||
it("should use node when ownedName is not exactly `eth`", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it("should use node when ownedName is not exactly `eth`", () => { | |
it("should use node when ownedName is not `eth`", () => { |
apps/ensnode/src/lib/ids.ts
Outdated
* @param label registration's label | ||
* @param node registration's node | ||
*/ | ||
export const makeRegistrationId = (ownedName: OwnedName, label: Labelhash, node: Node) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const makeRegistrationId = (ownedName: OwnedName, label: Labelhash, node: Node) => | |
export const makeRegistrationId = (ownedName: OwnedName, labelHash: Labelhash, node: Node) => |
Let's please always be specific in the differences between a label
and a labelHash
, both as types and as variable names.
Lots of issues and mistakes are more likely to happen otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
based on my understanding of the terminology, a nitpick: labelhash
over labelHash
for these variables and typenames
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shrugs Worry here isn't for capitalization. Instead the issue here is I've noticed several places in the code where we are using "label" where we should instead be using "labelHash". There's an important distinction there.
apps/ensnode/src/lib/ids.ts
Outdated
* collisions that would otherwise occur. | ||
* | ||
* @param ownedName the ownedName of the ENSNode plugin that is processing the registration event | ||
* @param label registration's label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param label registration's label | |
* @param labelHash the labelHash of the direct subname of `registrarName` that was registered |
Please note here how the definition of this param has a relation to registrarName
.
apps/ensnode/src/lib/ids.ts
Outdated
* | ||
* @param ownedName the ownedName of the ENSNode plugin that is processing the registration event | ||
* @param label registration's label | ||
* @param node registration's node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param node registration's node | |
* @param node the node of the full name that was registered |
apps/ensnode/src/lib/ids.ts
Outdated
* any other Registrar use node (i.e. namehash(test.base.eth) to avoid | ||
* collisions that would otherwise occur. | ||
* | ||
* @param ownedName the ownedName of the ENSNode plugin that is processing the registration event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param ownedName the ownedName of the ENSNode plugin that is processing the registration event | |
* @param registrarName the name of the registrar issuing the registration |
Also suggest changing the type of registrarName
from OwnedName
to just a regular string
. Otherwise we're mixing up the concept of a registrar's name from a plugin's name. For more details please see the issues I flagged inside #87
apps/ensnode/src/lib/ids.ts
Outdated
/** | ||
* Makes a cross-chain unique registration ID. | ||
* | ||
* A Registration record's id is historically the labelhash of the label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really appreciate the efforts here! This is good and helps a lot 💪 I had a few suggestions while reviewing this. I thought it would be more efficient to share this feedback in a separate PR for you to review: #88
0386066
to
7cb6fc2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tk-o Thanks for updates. A few follow ups.
apps/ensnode/test/ids.spec.ts
Outdated
@@ -17,4 +17,18 @@ describe("ids", () => { | |||
expect(makeEventId(123n, 456)).toEqual("123-456"); | |||
}); | |||
}); | |||
|
|||
describe("makeRegistrationId", () => { | |||
it("should use labelhash when ownedName is `eth` to ensure subgraph compatibility", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest we change the text here now that the ownedName param is renamed
apps/ensnode/test/ids.spec.ts
Outdated
); | ||
}); | ||
|
||
it("should use node when ownedName is not `eth`", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see related comment above
apps/ensnode/src/lib/ids.ts
Outdated
* the .eth Registrar, this leaves no room in the namespace for Registration | ||
* records from other Registrars (like the base.eth and linea.eth Registrars). | ||
* To account for this, while preserving backwards compatibility, Registration | ||
* records created for the .eth Registrar use label as id and those created by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
labelhash
apps/ensnode/src/lib/ids.ts
Outdated
* @param node the node of the full name that was registered | ||
*/ | ||
export const makeRegistrationId = (registrarName: string, labelHash: Labelhash, node: Node) => | ||
registrarName === "eth" ? labelHash : node; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer to keep the inline comments I proposed in the separate PR. Ex: It matters that we explicitly defend details of how this function is implemented.
Proposes refined documentation for makeRegistrationId.
f1254dc
to
4f775ff
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tk-o Looks great! Approved 🚀
This PR fixes a primary key constraint issue on
registrations
table in database. All names registered for plugins, except for theeth
plugin, will use node value (namehash function output) for registration ID. Theeth
plugin will continue using registration IDs based of the label value only.Before this change, all registrations would have their respective ID derived from the label only. Which means that there would be multiple names competing for the same registration (i.e.
www.vitalik.eth
&www.vitalik.linea.eth
). In consequence, subgraph-compatible indexing logic would assume that for an existing (conflicting) registration entity, there must be a related domain entity.That was not always the case, and the runtime error would occur. Not anymore.
Fixes: