Skip to content
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

Loki name service - Multiple owners #1048

Closed
wants to merge 17 commits into from

Conversation

Doy-lee
Copy link
Collaborator

@Doy-lee Doy-lee commented Feb 27, 2020

Allows a primary and backup public key (either Monero twist, public key or a standard ed25519 key) to be the owner of a LNS record. If you are the owner, you are able to produce valid signatures to update the underlying value of the LNS record.

These maybe monero, maybe ed25519 keys are stored into crypto::generic_public_key and similarly for signatures into crypto::generic_signature.

I've also added lns_make_update_mapping_signature which allows wallets to generate the required signature standalone, so now other wallets that aren't owners- can update mappings on behalf of the owner wallet- provided that the owner wallet sends them the appropriate signature and new value they wish to update to.

@Doy-lee Doy-lee force-pushed the LokiNameServiceMultipleOwners branch 2 times, most recently from 2820429 to 8de1352 Compare March 4, 2020 05:25
unsigned char data[sizeof(ed25519_public_key)];
static constexpr generic_public_key null() { return {0}; }
operator bool() const { return memcmp(data, null().data, sizeof(data)); }
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a textbook case for using a variant.

I look at unions with skepticism in C++ code, but these ones are particularly dirty because the code is also throwing away (important!) key/signature type information, which is never a good thing. Aside from the C++ type safety, this also throws away the type safety in the encoding of the value so that we have no idea what type of public key or signature we actually have when we reconstruct it. As a result, the verify ends up trying one and then trying the other, and that feels very wrong to me.

The clean solution here is to use a variant<ed25519_public_key, public_key> (either boost or mapbox from the lokimq PR) and similarly for signatures; when encoding you then use a flag and to use a flag when serializing/deserializing to encode the type information so that you can reload the variant with the proper type on the other end. Thus you never lose the key/signature type, and you never have to "guess" by trying to do a verify against whatever type it could be: signature verification is just a visitor that does the correct signature verification call. (Plus a variant of hashable types is itself hashable so you can still use as an unordered map key, which looks like it was part of the desire here).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer just adding a enum struct type/bool is_monero flag to the structure itself to keep the information together with the type rather than variant because the codegen for variant sucks. With the variant method you also decouple the type from the data for de/serialisation?

If that's not an issue with you then I'm ok with switching over to variant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because the codegen for variant sucks.

Can you elaborate? AFAIK most of the codegen issues with variant are around visiting, which you wouldn't need to use here (just get_ifs for the two types).

With the variant method you also decouple the type from the data for de/serialisation?

Sure, but you always do that with serialization, deserialization should pick it up again (which means the serialization needs to carry the type as well).

Copy link
Collaborator Author

@Doy-lee Doy-lee Mar 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate? AFAIK most of the codegen issues with variant are around visiting, which you wouldn't need to use here (just get_ifs for the two types).

Yes. You're somewhat right its good at -O2 for the std implementation but boost is just crazy, https://godbolt.org/z/awP_wd

Sure, but you always do that with serialization, deserialization should pick it up again (which means the serialization needs to carry the type as well).

Yeah actually not a big deal after some thought as it can easily be achieved

struct generic_key 
{
    variant<a, b> key;
    bool is_a;
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loki-mq is using mapbox::variant because most things in boost are too heavy.

{
if (!crypto::check_signature(hash, key.monero, signature.monero))
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, this should be a type-safe visitor over a variant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that, it can just probe the types with an if/else rather than doing a full visit.

*reason = err_stream.str();
}
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have this same basic block of code (with some small variations) 33 times in this file (and also there's a bug in the "trying to renew too early" case about 150 lines above this, which isn't checking whether reason is set). You can DRY it out a lot like this:

template <typename... T>
static bool check_condition(bool condition, std::string* reason, T&&... args) {
  if (!condition && reason)
  {
    std::ostringstream o;
#ifdef __cpp_fold_expressions // C++17
    (o << ... << std::forward<T>(args));
#else
    (void) std::initializer_list<int>{(os << std::forward<T>(args), 0)...};
#endif
    *reason = o.str();
  }
  return condition;
}

and then all of these check-and-set-reason blocks compact hugely to just:

    if (!check_condition(entry->signature, reason, tx, ", ", *entry, ", signature to validate is the null signature"))
      return false;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix up the typo and apply the change in the other PR since some messaging/new errors have been added.

WHERE NOT EXISTS
(SELECT * FROM "mappings"
WHERE "owner"."id" = "mappings"."owner_id"
OR "owner"."id" = "mappings"."backup_owner_id"))";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query is going to have to do a full table scan currently; it needs some indexes added on mappings.owner_id and mappings.backup_owner_id.

(I don't have a lot of experience with sqlite's query optimizer; it might also benefit from being rewritten as WHERE NOT EXISTS (... = owner_id) AND NOT EXISTS (... = backup_owner_id), but in either case it'll still want the indices.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added what I think you meant by this with

+CREATE INDEX IF NOT EXISTS "owner_id_index" ON mappings("owner_id");
+CREATE INDEX IF NOT EXISTS "backup_owner_id_index" ON mappings("backup_owner_index");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

char constexpr SAVE_MAPPING_SQL[] = R"(INSERT OR REPLACE INTO "mappings" ("type", "name", "value", "txid", "prev_txid", "register_height", "owner_id") VALUES (?,?,?,?,?,?,?))";
char constexpr SAVE_OWNER_SQL[] = R"(INSERT INTO "owner" ("public_key") VALUES (?);)";
char constexpr SAVE_SETTINGS_SQL[] = R"(INSERT OR REPLACE INTO "settings" ("id", "top_height", "top_hash", "version") VALUES (1,?,?,?))";
std::string const get_mappings_by_owner_str = sql_cmd_combine_mappings_and_owner_table(R"(WHERE "o1"."public_key" = ? OR "o2"."public_key" = ?)");
Copy link
Member

@jagerman jagerman Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid needing to double-bind the value when you use this in get_mappings_by_owner by rewriting this condition as:
WHERE ? IN ("o1"."public_key", "o2"."public_key")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

PRINT_USAGE(USAGE_BUY_LNS_MAPPING);
fail_msg_writer() << "lns name didn't start or end with quotation marks (')";
PRINT_USAGE(USAGE_LNS_BUY_MAPPING);
fail_msg_writer() << "lns name didn't start or end with quotation marks (\"), first word in name is=\"" << local_args[first_word_index] << "\"";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forcing quotation marks is kind of strange when the vast majority of registrations (and all lokinet registration names) are going to be single word values. Could we change this logic to:

  • if first_word_index == last_word_index, then don't require quotes (but remove them if present on front and back).
  • otherwise keep the quotes required logic.

Also the quoted value here is sort of... weird (because of how simplewallet parses arguments): trying to register

"foo   bar"

will actually register

foo bar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually: why do we allow spaces in names at all? It seems like it is going to break a huge ton of things. For example, if someone has a tag "a b" then what happens when someone tags their user name in Session?

@a
@a b
@a b c

What if a and a b and a b c are all registered? Or what if Session sees @word another word here blah blah, will it have to look up word and then word another and then ...?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually looking some more - we do zero validation on session names right now, which doesn't seem right. (Control characters? Whitespace? Non-normalized UTF-8? Invalid UTF-8 sequences?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(These comments about allowed characters in Session LNS names are aimed a bit more generally - @KeeJef @gmbnt)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine if we just following the hostname naming conventions for usernames and domain names https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_hostnames

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought this up earlier about the perils of utf8 whitespacing in names but no one raised an issue :(

I'll validate this as per the wiki in a new PR.

return true;
}
//----------------------------------------------------------------------------------------------------
static char constexpr NULL_KEY_STR[] = "0000000000000000000000000000000000000000000000000000000000000000";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be more user friendly if we just printed "(none)" instead of a bunch of 0s.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that'd be good.

return true;
}

std::string const &new_value = args[args.size() - 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args.back()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return false;

std::map<crypto::ed25519_public_key, size_t> key_to_request_index;
std::vector<crypto::ed25519_public_key> keys;
std::map<crypto::generic_public_key, size_t> key_to_request_index;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an unordered_map

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general the number of keys is going to be small, so I opted for a tree?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unordered_map on our pubkey types is actually really fast because we don't actually have to hash anything, we just use the value itself as a hash. But you're right, for a small container it's not going to matter anyway.


In future, support for mappings on Blockchain wallets and Lokinet addresses will be available. Tentatively,
- for Wallets, the recommended owner is the monero ed25519 public key of the user's wallet spend key set to loki_owner
- for Lokinet, the recommended default owner is the ed25519 public key of the user's Lokinet key set to ed_owner
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the "for Lokinet" part here really makes sense. Lokinet doesn't really expose your keys for something like this; it makes more sense to keep it as the wallet keys here, as well: so Session recommends the session public key, and everything else recommends the wallet public key.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay updated

@Doy-lee Doy-lee force-pushed the LokiNameServiceMultipleOwners branch 2 times, most recently from f4822b5 to 528e995 Compare March 10, 2020 08:53
@Doy-lee Doy-lee force-pushed the LokiNameServiceMultipleOwners branch from 528e995 to 5e4c443 Compare March 10, 2020 23:57
@Doy-lee
Copy link
Collaborator Author

Doy-lee commented Mar 11, 2020

This was merged in: #1051

@Doy-lee Doy-lee closed this Mar 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants