Open nlordell opened 1 year ago
I've thought about the signatures today. In any case, we're going to have some kind of public key cryptography scheme that allows us to ensure only the solver produced the message. Naturally that can be secp256k because that is used in the Ethereum ecosystem already. The harder question is what exactly the signed message should be:
The advantage of 3 is that we don't have to come up with any encoding. The driver signs exactly what it has sent. The downside of this is that it means we have to keep those bytes around too because that is the only way to get back to the original message. For example, after parsing into natural Rust types, we would still need to keep the original Vec<u8>
around and store it in the database to later prove that this is what was sent.
3 requires a more awkward API specification too because we can't store the signature and the signed data in the same json struct because the signature depends on the encoding. If we want the API to return a single json object then we would need to double encode the message which is weird. For example, we would return an object { signature: "...", data: "..." }
where the content of data is itself a string that is json encoded object.
1 and 2 apply a transformation on the high level data to turn it into signable bytes. This has the advantage that we can handle the data in whatever way is natural while keeping the ability to validate its signature.
A downside of 1 is that EIP-712 doesn't have an easy to use Rust library. We'd have to implement our own or do the encoding by hand. If we have a type like a BigInteger we would still need to model how it should be represented by EIP-712 types. The advantage is that with an use EIP-712 library, it would be easy to change the message type later.
With 2 we would pick our own encoding. This might be better than 1 because it can be simpler than full EIP-712. I would consider this if it turns out that the data we care about having signed is very simple. For example, the data that needs to be signed for /solve is only the auction id and the promised objective value. This is easy to manually transform into bytes.
But if we need to sign the full auction struct in /execute then this becomes unreasonable. Maybe for /execute we only need to sign a message like "i promise to execute
It might not be obvious why you can't "just sign the json". This is partially explained in EIP-712. A short example of the problem is considering signing a single json integer. Signing usually works on raw bytes. The interface could look like fn sign(PrivateKey, data: &[u8]) -> Signature
and fn validate_signature(PublicKey, data: &[u8]) -> bool
.
How do we turn the json integer into bytes to use in this api? The naive approach is to use whatever json serializer you have and sign the serialized bytes. The problem is that in json the integers 42
, 042
, 42.0
are all semantically the same. If I receive the json integer, deserialize it into a Rust u64, store that in my database, then there is no guarantee that I will later be able to recreate the original message that was signed.
This is why in approach 3, the original message needs to be kept around.
Niksha found an existing json signing standard like EIP-712 https://en.wikipedia.org/wiki/JSON_Web_Signature . This is another consideration.
This is how I imagine the process to go:
The driver already has a keypair, which it uses for broadcasting transactions. It can reuse this keypair or generate a new one specifically for the purpose of signing messages - I think either approach is fine.
The autopilot will have a keypair specifically for the purpose of singing messages.
The raw string of the body of each request that the autopilot makes will be signed using the autopilot keypair. The signature will be in a header, e.g. X-Signature
. The raw string of the body of each response that the driver returns is likewise signed with the driver keypair and specified in the X-Signature
header of the response.
This way the autopilot and driver use signatures to authenticate themselves between each other. The signatures can also be used by either party at a later point in time to prove that they received a certain request/response from the other party.
The downside is that all data is signed in its raw format. Of course the signature will be over a hash of the data, but the problem is that if a party wants to be able to prove at a later time that they did receive a certain request, the party has to store all of the raw data of the request, which might not be the best choice for storage efficiency (i.e. it might be too wasteful).
The upside is that the protocol is simple and that the signature can be checked before anything is done with the data, so we avoid the doom principle. The extra storage required is unlikely to actually be a problem in practice since storage is comparatively cheap.
Not part of the initial deliverable scope (not sure if we want to keep this issue around or close it for now)
Something we don't need for now, but will revisit once decentralized drivers become a thing.
This issue has been marked as stale because it has been inactive a while. Please update this issue or it will be automatically closed.
This issue has been marked as stale because it has been inactive a while. Please update this issue or it will be automatically closed.
Communication protocol
The driver already has a keypair, which it uses for broadcasting transactions. It can reuse this keypair or for the purpose of signing messages.
The autopilot will have a single keypair specifically for the purpose of singing messages. We will also make our pubkey known to the solvers and the community.
The raw string of the body of each request that the autopilot makes will be signed using the autopilot keypair. The signature will be in a header, e.g.
X-Signature
. The raw string of the body of each response that the driver returns will likewise be signed with the driver keypair and specified in theX-Signature
header of the response.This way the autopilot and driver will use signatures to authenticate themselves between each other. The signatures can also be used by either party at a later point in time to prove that they received a certain request/response from the other party.
Signatures
The protocol will use
secp256k1
signatures over thekeccak256
hash of the raw bytes of the body data encoded as UTF-8.Tradeoffs
The downside is that all data is signed in its raw format. Of course the signature will be over a hash of the data, but the problem is that if a party wants to be able to prove at a later time that they did receive a certain request, they have to store all of the raw data of the request. In practice this is unlikely to be an issue, since storage tends to be relatively cheap.
The upside is that the protocol is simple and the signature can be checked before anything is done with the data, so we avoid the doom principle.