Skip to content

Commit

Permalink
2879 tzip32 doc (#2994)
Browse files Browse the repository at this point in the history
* docs: tzip-32 signing documentation

* docs: updated examples in tab and and v20 versioned docs

* docs: addressed review comments

* test: add signing.md file changes

* test: fixing integration test
  • Loading branch information
hui-an-yang authored Jul 16, 2024
1 parent 22d8aea commit bb06ee6
Show file tree
Hide file tree
Showing 6 changed files with 4,046 additions and 8,276 deletions.
84 changes: 84 additions & 0 deletions docs/signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ id: signing
author: Claude Barde
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Signing arbitrary chunks of data is a common practice in a blockchain environment and is usually done to prove that a user has access to a certain account or that a message comes from a certain account.

This practice is still new on Tezos and the use cases are rare. However, as the interactions between users and smart contracts increase, knowing how to sign data and send the signature to a smart contract can set you one step ahead in your knowledge of the Tezos blockchain.
Expand Down Expand Up @@ -141,6 +144,87 @@ const isVerified = verifySignature(
);
```

## Generating a TZIP-32 message signature

The community have proposed [TZIP-32](https://gitlab.com/tezos/tzip/-/blob/71be45d3ae2e15cec5c7a2f84feb88aac58fbe5e/drafts/current/draft-message-signing/tzip-32.md)(draft), Off-Chain Message Signing, which aims to define a formal message signing standard that is simple, secure, extendable and compatible with hardware wallets. TZIP-32 can be further used in [TZIP-33](https://gitlab.com/tezos/tzip/-/blob/6483efc9e591960effe76f4ae996ec187bf13bf4/drafts/current/draft-sign-in-with-tezos.md)(draft) which is a [CAIP-122](https://chainagnostic.org/CAIPs/caip-122) compliant sign-in with Tezos standard.

**Message encoding**

This is the schema to encode the message into bytes, please read the [TZIP-32](https://gitlab.com/tezos/tzip/-/blob/71be45d3ae2e15cec5c7a2f84feb88aac58fbe5e/drafts/current/draft-message-signing/tzip-32.md) for more detail.

| Name | Size | Contents |
|-----------------------|----------|-------------------------|
| magic_string | 30 bytes | bytes |
| # Bytes in next field | 1 byte | unsigned 8-bit integer |
| interface | variable | bytes |
| character_encoding | 1 byte | unsigned 8-bit integer |
| # Bytes in next field | 2 bytes | unsigned 16-bit integer |
| message | Variable | bytes |

**Examples of signing TZIP-32 message on contractAPI and walletAPI**

The Off-Chain Message Signing magic bytes is `0x80` defined in [TZIP-31](https://gitlab.com/tezos/tzip/-/blob/71be45d3ae2e15cec5c7a2f84feb88aac58fbe5e/drafts/current/draft-signer-requests/tzip-31.md)(draft).

<Tabs
defaultValue="contractAPI"
values={[
{label: 'Contract API', value: 'contractAPI'},
{label: 'Wallet API', value: 'walletAPI'}
]}>
<TabItem value="contractAPI">

```js live noInline
// import { TezosToolkit } from '@taquito/taquito'
// import { InMemorySigner } from '@taquito/signer'
// import { stringToBytes, num2PaddedHex } from '@taquito/utils';
// const Tezos = new TezosToolkit('https://ghostnet.ecadinfra.com');

let magicByte = '0x80'
let magicString = 'tezos signed offchain message'
let interface_ = 'tzip://32'
let characterEncoding = '0'
let message = 'Hello world!'

let bytes = stringToBytes(magicString) + num2PaddedHex(interface_.length, 8) + stringToBytes(interface_) + num2PaddedHex(Number(characterEncoding), 8) + num2PaddedHex(message.length, 16) + stringToBytes(message)

InMemorySigner.fromSecretKey('edsk2rKA8YEExg9Zo2qNPiQnnYheF1DhqjLVmfKdxiFfu5GyGRZRnb')
.then((theSigner) => {
Tezos.setProvider({ signer: theSigner });
return Tezos.signer.sign(bytes, new Uint8Array([parseInt(magicByte, 16)]))
})
.then(signed => {
println(JSON.stringify(signed, null, 2));
})
.catch((error) => println(`Error: ${error} ${JSON.stringify(error, null, 2)}`));
```

</TabItem>
<TabItem value="walletAPI">

```js live noInline wallet
// import { TezosToolkit } from '@taquito/taquito'
// import { InMemorySigner } from '@taquito/signer'
// import { stringToBytes, num2PaddedHex } from '@taquito/utils';
// const Tezos = new TezosToolkit('https://ghostnet.ecadinfra.com');

let magicByte = '0x80'
let magicString = 'tezos signed offchain message'
let interface_ = 'tzip://32'
let characterEncoding = '0'
let message = 'Hello world!'

let bytes = stringToBytes(magicString) + num2PaddedHex(interface_.length, 8) + stringToBytes(interface_) + num2PaddedHex(Number(characterEncoding), 8) + num2PaddedHex(message.length, 16) + stringToBytes(message)

const payload = {
signingType: SigningType.RAW,
payload: magicByte + bytes
}
wallet.client.requestSignPayload(payload).then(signed => println(JSON.stringify(signed, null, 2)))
```

</TabItem>
</Tabs>

## Signing Michelson data

Taquito also offers the possibility to sign Michelson code. This feature can be useful, for example, if you need to send a lambda to a contract to be executed but want to restrict the number of users who can submit a lambda by verifying the signer's address. The signing of Michelson code requires the use of the `michel-codec` package:
Expand Down
82 changes: 41 additions & 41 deletions integration-tests/__tests__/contract/estimation-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,23 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
const estimate = await LowAmountTez.estimate.transfer({ to: await Tezos.signer.publicKeyHash(), amount: 0.019 });
expect(estimate.gasLimit).toEqual(101);
expect(estimate.storageLimit).toEqual(0);
expect(estimate.suggestedFeeMutez).toEqual(186);
expect(estimate.suggestedFeeMutez).toEqual(188);
expect(estimate.burnFeeMutez).toEqual(0);
expect(estimate.minimalFeeMutez).toEqual(166);
expect(estimate.totalCost).toEqual(166);
expect(estimate.usingBaseFeeMutez).toEqual(166);
expect(estimate.minimalFeeMutez).toEqual(168);
expect(estimate.totalCost).toEqual(168);
expect(estimate.usingBaseFeeMutez).toEqual(168);
expect(estimate.consumedMilligas).toEqual(100040);
});

it('Verify .estimate.transfer with unallocated destination', async () => {
const estimate = await LowAmountTez.estimate.transfer({ to: await (await createAddress()).signer.publicKeyHash(), amount: 0.017 });
expect(estimate.gasLimit).toEqual(101);
expect(estimate.storageLimit).toEqual(277);
expect(estimate.suggestedFeeMutez).toEqual(186);
expect(estimate.suggestedFeeMutez).toEqual(188);
expect(estimate.burnFeeMutez).toEqual(69250);
expect(estimate.minimalFeeMutez).toEqual(166);
expect(estimate.totalCost).toEqual(69416);
expect(estimate.usingBaseFeeMutez).toEqual(166);
expect(estimate.minimalFeeMutez).toEqual(168);
expect(estimate.totalCost).toEqual(69418);
expect(estimate.usingBaseFeeMutez).toEqual(168);
expect(estimate.consumedMilligas).toEqual(100040);
});

Expand All @@ -69,11 +69,11 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
});
expect(estimate.gasLimit).toEqual(677);
expect(estimate.storageLimit).toEqual(591);
expect(estimate.suggestedFeeMutez).toEqual(535);
expect(estimate.suggestedFeeMutez).toEqual(537);
expect(estimate.burnFeeMutez).toEqual(147750);
expect(estimate.minimalFeeMutez).toEqual(515);
expect(estimate.totalCost).toEqual(148265);
expect(estimate.usingBaseFeeMutez).toEqual(515);
expect(estimate.minimalFeeMutez).toEqual(517);
expect(estimate.totalCost).toEqual(148267);
expect(estimate.usingBaseFeeMutez).toEqual(517);
expect(estimate.consumedMilligas).toEqual(676402);
});

Expand All @@ -84,11 +84,11 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
});
expect(estimate.gasLimit).toEqual(100);
expect(estimate.storageLimit).toEqual(0);
expect(estimate.suggestedFeeMutez).toEqual(181);
expect(estimate.suggestedFeeMutez).toEqual(183);
expect(estimate.burnFeeMutez).toEqual(0);
expect(estimate.minimalFeeMutez).toEqual(161);
expect(estimate.totalCost).toEqual(161);
expect(estimate.usingBaseFeeMutez).toEqual(161);
expect(estimate.minimalFeeMutez).toEqual(163);
expect(estimate.totalCost).toEqual(163);
expect(estimate.usingBaseFeeMutez).toEqual(163);
expect(estimate.consumedMilligas).toEqual(100000);
});

Expand All @@ -97,12 +97,12 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
const estimate = await LowAmountTez.estimate.transfer(tx);
expect(estimate.gasLimit).toEqual(1457);
expect(estimate.storageLimit).toEqual(0);
expect(estimate.suggestedFeeMutez).toEqual(394);
expect(estimate.suggestedFeeMutez).toEqual(396);
expect(estimate.burnFeeMutez).toEqual(0);
expect(estimate.minimalFeeMutez).toEqual(374);
expect(estimate.totalCost).toEqual(374);
expect(estimate.usingBaseFeeMutez).toEqual(374);
expect(estimate.consumedMilligas).toEqual(1456142);
expect(estimate.minimalFeeMutez).toEqual(376);
expect(estimate.totalCost).toEqual(376);
expect(estimate.usingBaseFeeMutez).toEqual(376);
expect(estimate.consumedMilligas).toEqual(1456228);
});

it('Verify .estimate.transfer for multiple internal transfers to unallocated account', async () => {
Expand All @@ -114,38 +114,38 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
const estimate = await LowAmountTez.estimate.transfer(tx);
expect(estimate.gasLimit).toEqual(1571);
expect(estimate.storageLimit).toEqual(534);
expect(estimate.suggestedFeeMutez).toEqual(465);
expect(estimate.suggestedFeeMutez).toEqual(467);
expect(estimate.burnFeeMutez).toEqual(133500);
expect(estimate.minimalFeeMutez).toEqual(445);
expect(estimate.totalCost).toEqual(133945);
expect(estimate.usingBaseFeeMutez).toEqual(445);
expect(estimate.consumedMilligas).toEqual(1570671);
expect(estimate.minimalFeeMutez).toEqual(447);
expect(estimate.totalCost).toEqual(133947);
expect(estimate.usingBaseFeeMutez).toEqual(447);
expect(estimate.consumedMilligas).toEqual(1570757);
});

it('Verify .estimate.transfer for internal origination', async () => {
const tx = contract.methods.do(originate()).toTransferParams();
const estimate = await LowAmountTez.estimate.transfer(tx);
expect(estimate.gasLimit).toEqual(1867);
expect(estimate.storageLimit).toEqual(337);
expect(estimate.suggestedFeeMutez).toEqual(441);
expect(estimate.suggestedFeeMutez).toEqual(443);
expect(estimate.burnFeeMutez).toEqual(84250);
expect(estimate.minimalFeeMutez).toEqual(421);
expect(estimate.totalCost).toEqual(84671);
expect(estimate.usingBaseFeeMutez).toEqual(421);
expect(estimate.consumedMilligas).toEqual(1866766);
expect(estimate.minimalFeeMutez).toEqual(423);
expect(estimate.totalCost).toEqual(84673);
expect(estimate.usingBaseFeeMutez).toEqual(423);
expect(estimate.consumedMilligas).toEqual(1866852);
});

it('Verify .estimate.transfer for multiple internal originations', async () => {
const tx = contract.methods.do(originate2()).toTransferParams();
const estimate = await LowAmountTez.estimate.transfer(tx);
expect(estimate.gasLimit).toEqual(2392);
expect(estimate.gasLimit).toEqual(2393);
expect(estimate.storageLimit).toEqual(654);
expect(estimate.suggestedFeeMutez).toEqual(559);
expect(estimate.suggestedFeeMutez).toEqual(561);
expect(estimate.burnFeeMutez).toEqual(163500);
expect(estimate.minimalFeeMutez).toEqual(539);
expect(estimate.totalCost).toEqual(164039);
expect(estimate.usingBaseFeeMutez).toEqual(539);
expect(estimate.consumedMilligas).toEqual(2391919);
expect(estimate.minimalFeeMutez).toEqual(541);
expect(estimate.totalCost).toEqual(164041);
expect(estimate.usingBaseFeeMutez).toEqual(541);
expect(estimate.consumedMilligas).toEqual(2392005);
// Do the actual operation
const op2 = await contract.methods.do(originate2()).send();
await op2.confirmation();
Expand Down Expand Up @@ -176,11 +176,11 @@ CONFIGS().forEach(({ lib, setup, knownBaker, createAddress, rpc }) => {
let estimate = await LowAmountTez.estimate.transfer({ to: await Tezos.signer.publicKeyHash(), mutez: true, amount: amt - (1382 + getRevealFee(await LowAmountTez.signer.publicKeyHash())) });
expect(estimate.gasLimit).toEqual(101);
expect(estimate.storageLimit).toEqual(0);
expect(estimate.suggestedFeeMutez).toEqual(185);
expect(estimate.suggestedFeeMutez).toEqual(187);
expect(estimate.burnFeeMutez).toEqual(0);
expect(estimate.minimalFeeMutez).toEqual(165);
expect(estimate.totalCost).toEqual(165);
expect(estimate.usingBaseFeeMutez).toEqual(165);
expect(estimate.minimalFeeMutez).toEqual(167);
expect(estimate.totalCost).toEqual(167);
expect(estimate.usingBaseFeeMutez).toEqual(167);
expect(estimate.consumedMilligas).toEqual(100040);
});

Expand Down
2 changes: 1 addition & 1 deletion integration-tests/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const defaultConfig = ({
rpc: process.env[`TEZOS_RPC_${networkName}`] || defaultRpc,
pollingIntervalMilliseconds: process.env[`POLLING_INTERVAL_MILLISECONDS`] || undefined,
rpcCacheMilliseconds: process.env[`RPC_CACHE_MILLISECONDS`] || '1000',
knownBaker: process.env[`TEZOS_BAKER`] || (networkName === 'WEEKLYNET' ? 'tz1ck3EJwzFpbLVmXVuEn5Ptwzc6Aj14mHSH' : 'tz1TnEtqDV9mZyts2pfMy6Jw1BTPs4LMjL8M'), // tz1TnEtqDV9mZyts2pfMy6Jw1BTPs4LMjL8M aka teztnetsbaker is a placeholder for tz1cjyja1TU6fiyiFav3mFAdnDsCReJ12hPD on parisc protocol
knownBaker: process.env[`TEZOS_BAKER`] || (networkName === 'WEEKLYNET' ? 'tz1ck3EJwzFpbLVmXVuEn5Ptwzc6Aj14mHSH' : 'tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzb'), // tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzbn(Germán - TT) is a placeholder for til tz1cjyja1TU6fiyiFav3mFAdnDsCReJ12hPD accepts external staking
knownContract: process.env[`TEZOS_${networkName}_CONTRACT_ADDRESS`] || knownContracts.contract,
knownBigMapContract: process.env[`TEZOS_${networkName}_BIGMAPCONTRACT_ADDRESS`] || knownContracts.bigMapContract,
knownTzip1216Contract: process.env[`TEZOS_${networkName}_TZIP1216CONTRACT_ADDRESS`] || knownContracts.tzip12BigMapOffChainContract,
Expand Down
Loading

0 comments on commit bb06ee6

Please sign in to comment.