Skip to content

Commit 61c78da

Browse files
committed
contracts: sr25519 support
1 parent a4069ee commit 61c78da

File tree

7 files changed

+204
-68
lines changed

7 files changed

+204
-68
lines changed

contracts/contracts/Sapphire.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ library Sapphire {
318318
* - `4` (`Secp256k1PrehashedKeccak256`)
319319
* - `5` (`Secp256k1PrehashedSha256`)
320320
*
321+
* ##### sr25519: 1,000 gas
322+
* - `6` (`sr25519`)
323+
*
321324
* ##### Secp256r1: 4,000 gas
322325
* - `7` (`Secp256r1PrehashedSha256`)
323326
*
@@ -343,6 +346,11 @@ library Sapphire {
343346
* byte X coordinate).
344347
* Secret key: 48 bytes
345348
*
349+
* ##### sr25519
350+
*
351+
* Public key: 32 bytes
352+
* Secret key: 96 bytes (64 byte secret key, 32 byte public key)
353+
*
346354
* #### Example
347355
*
348356
* ```solidity
@@ -401,6 +409,7 @@ library Sapphire {
401409
* (32 bytes) as context, empty message.
402410
* - `5` (`Secp256k1PrehashedSha256`): 3,000 gas, pre-existing hash (32
403411
* bytes) as context, empty message.
412+
* - `6` (`sr25519`): 1,500 gas, bytes (e.g. 'substrate) as context, arbitrary length message
404413
* - `7` (`Secp256r1PrehashedSha256`): 9,000 gas, pre-existing hash (32
405414
* bytes) as context, empty message.
406415
* - `8` (`Secp384r1PrehashedSha384`): 43,200 gas, pre-existing hash (32
@@ -462,6 +471,7 @@ library Sapphire {
462471
* - `3` (`Secp256k1Oasis`): 3,000 gas
463472
* - `4` (`Secp256k1PrehashedKeccak256`): 3,000 gas
464473
* - `5` (`Secp256k1PrehashedSha256`): 3,000 gas
474+
* - `6` (`sr25519`): 2,000 gas
465475
* - `7` (`Secp256r1PrehashedSha256`): 7,900 gas
466476
* - `8` (`Secp384r1PrehashedSha384`): 37,920 gas
467477
*

contracts/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
"contracts"
2626
],
2727
"devDependencies": {
28+
"@noble/hashes": "1.3.2",
2829
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
2930
"@nomicfoundation/hardhat-ethers": "^3.0.5",
3031
"@oasisprotocol/client": "^0.1.1-alpha.2",
32+
"@oasisprotocol/sapphire-ethers-v6": "workspace:^",
3133
"@oasisprotocol/sapphire-hardhat": "workspace:^",
3234
"@oasisprotocol/sapphire-paratime": "workspace:^",
33-
"@oasisprotocol/sapphire-ethers-v6": "workspace:^",
3435
"@typechain/ethers-v6": "^0.5.1",
3536
"@typechain/hardhat": "^9.1.0",
3637
"@types/chai": "^4.3.3",
@@ -45,6 +46,7 @@
4546
"ethers": "6.x",
4647
"hardhat": "^2.22.2",
4748
"hardhat-watcher": "^2.5.0",
49+
"micro-sr25519": "^0.1.0",
4850
"npm-run-all": "^4.1.5",
4951
"prettier": "^2.7.1",
5052
"prettier-plugin-solidity": "1.0.0-beta.24",
@@ -53,8 +55,7 @@
5355
"solidity-coverage": "^0.8.2",
5456
"ts-node": "^10.9.1",
5557
"typechain": "^8.3.2",
56-
"typescript": "^4.8.3",
57-
"@noble/hashes": "1.3.2"
58+
"typescript": "^4.8.3"
5859
},
5960
"dependencies": {
6061
"@openzeppelin/contracts": "^5.0.2"

contracts/test/signing.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ethers } from 'hardhat';
66

77
import { SigningTests__factory } from '../typechain-types/factories/contracts/tests';
88
import { SigningTests } from '../typechain-types/contracts/tests/SigningTests';
9+
import * as sr25519 from 'micro-sr25519';
10+
import { getBytes, hexlify, keccak256 } from 'ethers';
911

1012
function randomBytesUnlike(len: number, orig: Buffer): Buffer {
1113
do {
@@ -205,7 +207,107 @@ describe('Signing', function () {
205207
);
206208
});
207209

208-
// TODO: implement Sr25519
210+
it('sr25519', async () => {
211+
// Try sr25519 (alg=6)
212+
// 32 byte context, empty message
213+
const sha256_kp = await se.testKeygen(6, randomBytes(32));
214+
await testSignThenVerify(
215+
se,
216+
6,
217+
sha256_kp,
218+
randomBytes(32),
219+
EMPTY_BUFFER,
220+
32,
221+
0,
222+
);
223+
224+
// Key derivation from polkadot test cases
225+
// See: https://github.com/polkadot-js/wasm/blob/10010830094e7d033bd11b16c5e3bc01a7045309/packages/wasm-crypto/src/rs/sr25519.rs#L176
226+
const secretSeed = getBytes(
227+
'0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e',
228+
);
229+
const secretKey = sr25519.secretFromSeed(secretSeed);
230+
const publicKey = sr25519.getPublicKey(secretKey);
231+
expect(hexlify(publicKey)).eq(
232+
'0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a',
233+
);
234+
235+
// Known valid signature
236+
const msg = new TextEncoder().encode('<Bytes>message to sign</Bytes>');
237+
const sig = getBytes(
238+
'0x48ce2c90e08651adfc8ecef84e916f6d1bb51ebebd16150ee12df247841a5437951ea0f9d632ca165e6ab391532e75e701be6a1caa88c8a6bcca3511f55b4183',
239+
);
240+
const sigSigner = getBytes(
241+
'0xf84d048da2ddae2d9d8fd6763f469566e8817a26114f39408de15547f6d47805',
242+
);
243+
244+
// Verify JS implementation matches polkadot test case signature
245+
const isValid = sr25519.verify(msg, sig, sigSigner);
246+
expect(isValid).eq(true);
247+
248+
const CONTEXT = new TextEncoder().encode('substrate');
249+
250+
// Verify on-chain implementation also works
251+
const result = await se.testVerify(6, sigSigner, CONTEXT, msg, sig);
252+
expect(result).eq(true);
253+
254+
// Test key generation on-chian matches JS implementation
255+
const generatedKey = await se.testKeygen(6, secretSeed);
256+
expect(hexlify(getBytes(generatedKey.secretKey).slice(0, 64))).eq(
257+
hexlify(secretKey),
258+
);
259+
expect(generatedKey.publicKey).eq(hexlify(publicKey));
260+
261+
// 64 byte secret, appended with 32 byte public key
262+
expect(getBytes(generatedKey.publicKey).length).eq(32);
263+
expect(getBytes(generatedKey.secretKey).length).eq(96);
264+
expect(hexlify(getBytes(generatedKey.secretKey).slice(64))).eq(
265+
generatedKey.publicKey,
266+
);
267+
268+
// JS can verify on-chain signed message
269+
const onchainSigned = await se.testSign(
270+
6,
271+
generatedKey.secretKey,
272+
CONTEXT,
273+
msg,
274+
);
275+
const jsVerify = sr25519.verify(
276+
msg,
277+
getBytes(onchainSigned),
278+
getBytes(generatedKey.publicKey),
279+
);
280+
expect(jsVerify).eq(true);
281+
// And on-chain can verify on-chain signed message
282+
expect(
283+
await se.testVerify(
284+
6,
285+
generatedKey.publicKey,
286+
CONTEXT,
287+
msg,
288+
onchainSigned,
289+
),
290+
).eq(true);
291+
292+
// JS roundtrip with on-chain generated keypair
293+
const jsSigned = sr25519.sign(
294+
getBytes(generatedKey.secretKey).slice(0, 64),
295+
msg,
296+
);
297+
expect(sr25519.verify(msg, jsSigned, getBytes(generatedKey.publicKey))).eq(
298+
true,
299+
);
300+
301+
// on-chain verify JS signed message
302+
const onchainVerify = await se.testVerify(
303+
6,
304+
generatedKey.publicKey,
305+
CONTEXT,
306+
msg,
307+
jsSigned,
308+
);
309+
expect(onchainVerify).eq(true);
310+
});
209311

210312
it('Secp256r1 (Prehashed SHA256)', async () => {
211313
// Try Secp256r1 (alg=7)

0 commit comments

Comments
 (0)