-
Notifications
You must be signed in to change notification settings - Fork 25
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
feat!: opt-in V2-only records, IPIP-428 verification #234
Conversation
430cd08
to
464f380
Compare
I'm opening this for review. It would be great to get feedback regarding correctness and JavaScript-y-ness. |
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.
Thank you for cleaning this up @hacdias 👍
Found some divergence in Value
tests here and the ones we have in GO (details + other small nits inline).
const defaultCreateOptions: CreateOptions = { | ||
v1Compatible: true |
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.
ℹ️ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A path of /
probably isn't valid?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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 feat: ipns v2 and v2 record combination and tests 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 fix!: only accept CIDs, PeerIds or strings as values #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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
IPNSRecord
type.IPNSRecord
values 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