feat!: opt-in V2-only records, IPIP-428 verification#234
feat!: opt-in V2-only records, IPIP-428 verification#234achingbrain merged 14 commits intomasterfrom
Conversation
430cd08 to
464f380
Compare
|
I'm opening this for review. It would be great to get feedback regarding correctness and JavaScript-y-ness. |
| const defaultCreateOptions: CreateOptions = { | ||
| v1Compatible: true |
There was a problem hiding this comment.
ℹ️ tldr for drive-by readers: this keeps the existing default behavior so js-ipns library still produces V1+V2 records (as noted in IPIP-428)
|
@lidel I've addressed your feedback and we now normalize the value both at creation and reading time 😄 I think this is ready to go. |
There was a problem hiding this comment.
This PR makes create return an instance of an IPNSRecord class, but the class methods don't do anything that couldn't be done in the constructor.
It also does the work of validating/parsing on every method invocation which is inefficient.
Please refactor this PR to have IPNSRecord as an interface like IPNSEntry before it, then have .create return an implementation of this interface.
My preference would be for a simple object that has the interface properties, unless there's a good reason to have a class.
Finally - I think these changes could have been made in a backwards-compatible way by overloading the method signature like:
export interface IPNSEntry {
// ..fields
}
export interface IPNSEntryV2 {
// ..fields
}
export interface CreateOptions {
v1Compatible?: boolean
}
export interface CreateV2OrV1Options {
v1Compatible: true
}
export interface CreateV2Options {
v1Compatible: false
}
const defaultCreateOptions = {
v1Compatible: true
}
export async function create (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSEntry>
export async function create (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSEntryV2>
export async function create (peerId: PeerId, value: Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSEntry> {
// ...implementation
}
src/index.ts
Outdated
| } | ||
|
|
||
| try { | ||
| const cid = CID.parse(str) |
There was a problem hiding this comment.
Parsing the CID like this is not free - if we want to accept CIDs as an argument, we should accept a CID object instead of trying to parse it from a string.
There was a problem hiding this comment.
Again, we don't want to. But that's what's already happening (no validation of what value was whatsoever): #234 (comment) - this is just a way of ensuring that the value is a CID and building a content path out of it.
src/index.ts
Outdated
| const normalizeValue = (value: string | Uint8Array): string => { | ||
| const str = typeof value === 'string' ? value : uint8ArrayToString(value) | ||
|
|
||
| if (str.startsWith('/')) { |
There was a problem hiding this comment.
I think more validation needs to be done here, as written '/' will be accepted, as will '/foo', '/foo/bar/baz', etc.
There was a problem hiding this comment.
I think these are valid.
Value can be "can be any path", so IPNS Records should be useful beyond /ipfs and /ipns paths.
IPNS should be concerned only about value being a valid CID or a path (arbitrary string starting with /)
There was a problem hiding this comment.
A path of / probably isn't valid?
There was a problem hiding this comment.
Depends: it would error if IPNS name is resolved by IPFS Gateway, but there could be JS app that uses IPNS records for publishing application-specific paths, and in that use case / or /foo could be ok.
(namely, we have CBOR Data field where people can now put arbitrary data, so users may end up putting noop / in the Value field)
src/index.ts
Outdated
| try { | ||
| const cid = CID.parse(str) | ||
| return '/ipfs/' + cid.toV1().toString() | ||
| } catch (_) { |
There was a problem hiding this comment.
If ignoring the error the following can be used:
| } catch (_) { | |
| } catch { |
Though I think we should at least log it here.
Better yet, accept a CID argument so we re-use the validation from that class, then this try/catch can be removed.
I think the hardest thing here will be on marshalling. Right now, Maybe I can make some change to |
Co-authored-by: Marcin Rataj <lidel@lidel.org>
|
@achingbrain I mostly rewrote it using interfaces. Could you pls take a look? |
| */ | ||
| export const validate = async (publicKey: PublicKey, entry: IPNSEntry): Promise<void> => { | ||
| const { value, validityType, validity } = entry | ||
| export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise<void> => { |
There was a problem hiding this comment.
We cannot allow the interface here as it abstracts us from the actual protobuf. We need to validate the protobuf.
This module has accepted `Uint8Array`s as values to store in IPNS records which means it has been mis-used to create records with raw CID bytes. To ensure this doesn't happen in future, accept only CIDs, PeerIds or arbitrary path strings prefixed with `"/"`. BREAKING CHANGE: all /ipns/* keys are now encoded as base36 encoded CIDv1 libp2p-cid --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>
There was a problem hiding this comment.
- PR with IPIP-428 is merged, spec now includes test vectors: https://specs.ipfs.tech/ipips/ipip-0428/#test-fixtures
- Kubo runs gateway-conformance tests in CI, so it will benefit from these being included once ipfs/gateway-conformance#157 lands.
- Helia does not have gateway tests atm, which means tests in this PR are all we have and it comes with concern described in #255 (comment)
- 👉 I think the fastest way to remove risk is to include these static test vectors in this PR.
|
@lidel I've added the conformance tests, I think we're good to go here? |
There was a problem hiding this comment.
@achingbrain thank you for adding these, yes, looks good to me, feel free to merge and bubble up to Helia whenever you have time.
(small cosmetic suggestion below, but up to you, not a blocker)
Co-authored-by: Marcin Rataj <lidel@lidel.org>
For context, see: ipfs/specs#428. Similar thing has already been done in the Go side: ipfs/boxo#339.
IPNSRecordtype.IPNSRecordvalues come from the CBOR data field.Note that from the JS API point of view this is a breaking change as the public programmatic interface for creating/verifying IPNS Record has been changed.
Closes #217