def lock(self):
        """Lock specified (abstract) requirements into (concrete) candidates.

        The locking procedure consists of four stages:

        * Resolve versions and dependency graph (powered by ResolveLib).
        * Walk the graph to determine "why" each candidate came to be, i.e.
          what top-level requirements result in a given candidate.
        * Populate hashes for resolved candidates.
        * Populate markers based on dependency specifications of each
          candidate, and the dependency graph.
        """
        provider = self.get_provider()
        reporter = self.get_reporter()
        resolver = resolvelib.Resolver(provider, reporter)

        with vistir.cd(self.project.root):
            state = resolver.resolve(self.requirements)

        traces = trace_graph(state.graph)

        hash_cache = HashCache()
        for r in state.mapping.values():
            if not r.hashes:
                r.hashes = get_hashes(hash_cache, r)

        set_metadata(
            state.mapping, traces,
            provider.fetched_dependencies,
            provider.collected_requires_pythons,
        )

        lockfile = plette.Lockfile.with_meta_from(self.project.pipfile)
        lockfile["default"] = _collect_derived_entries(
            state, traces, self.default_requirements,
        )
        lockfile["develop"] = _collect_derived_entries(
            state, traces, self.develop_requirements,
        )
        self.project.lockfile = lockfile