Problem
There is no way to sign arbitrary bytestring data. It's only possible to sign data that can be encoded as UTF-8. This is a problem for dApps with smart contracts that require wallets to sign arbitrary nonsense such as hashes (and which don't support EIP-712/signTypedData). LocalEthereum is one of those dApps.
The problem lies in the fact the data needs to be encoded as JSON before it's encrypted. Converting to JSON requires that the input message be encoded as UTF-8.
Whenever we convert an arbitrary bytestring to UTF-8 and then back, we lose data; e.g.:
const originalHex = '6e4d387c925a647844623762ab3c4a5b3acd9540';
const encoded = Buffer.from(originalHex, 'hex').toString('utf8');
const decoded = Buffer.from(encoded, 'utf8');
const decodedHex = decoded.toString('hex');
console.log(originalHex === decodedHex); // false
And so, the wallet receives a corrupted message to sign if we do something like:
const bytestring = Buffer.from('6e4d387c925a647844623762ab3c4a5b3acd9540', 'hex');
await walletConnector.signMessage([address, bytestring.toString()]);
What web3.js does
WalletConnect's signMessage([address, data])
is similar to web3.eth.personal.sign(data, address)
, however only the latter works for bytestrings currently.
To sign a bytestring with web3, you can use web3.eth.personal.sign('0xabcd', address)
.
The specs of web3.eth.personal.sign
say that if the data
parameter is a plain string, it will be encoded to hex using web3.utils.utf8ToHex
. In other words, data
is expected to be a hex-encoded bytestring however the function allows UTF-8 strings for convenience.
In transport, the message is always hex-encoded (i.e. if a dApp asks to sign "hello", the web3 library will ask the wallet to sign "0x68656c6c6f").
Proposal 1
WalletConnect could do the same thing as web3. At the top of signMessage
, before the _formatRequest
call, it could convert params[1]
to a hex-encoded string if it isn't already. On the wallet end, it'll decode the message from hex when it gets the eth_sign
call request.
|
public async signMessage (params: any[]) { |
|
if (!this._connected) { |
|
throw new Error('Session currently disconnected') |
|
} |
|
|
|
const request = this._formatRequest({ |
|
method: 'eth_sign', |
|
params |
|
}) |
For example:
if (!isHexStrict(params[1])) {
params[1] = utf8ToHex(params[1])
}
const request = this._formatRequest({
(isHexStrict
and utf8ToHex
are simple functions in web3.utils
.)
Caveats
- This is a breaking change. It'll only work if both the dApp and the wallet app are on the same version.
- The wallet won't know whether the message is supposed to be a UTF-8 string, causing UI problems. It could make a guess, but that's ugly. The same problem exists for web3.personal.sign.
Proposal 2
Implement the change on the wallet side only. When the wallet sees a call request eth_sign
with a message beginning with "0x", it'll treat it as a bytestring.
This will (a) let the wallet know when it's signing a UTF-8 string; and (b) only cause a breaking change for WC dApps that sign "0xabcd" strings intended to be displayed as UTF-8.
Proposal 3
Create a new method called something like signByteMessage
and a new corresponding call request. This will resolve both of the caveats.