Somnia 백서

Somnia

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 11쪽

ensures that: * Each entry can be updated or replaced deterministically. * Developers can reference or fetch a specific record by key. * Duplicate writes can be prevented. #### Example: Creating a Data ID A Data ID can be created by hashing a string, typically by combining context and timestamp. ```typescript import { toHex } from 'viem' const dataId = toHex(`username-${Date.now()}`, { size: 32 }) console.log("Data ID:", dataId) ``` Example Output: ```bash Data ID: 0x757365726e616d652d31373239303239323435 ``` You can now use this ID to publish structured data to the blockchain. A Data ID ensures every record written is unique and can be referenced or updated deterministically. #### Example: Writing Data with a Schema and Data ID ```typescript import { SchemaEncoder } from '@somnia-chain/streams' const encoder = new SchemaEncoder(userSchema) const encodedData = encoder.encodeData([ { name: 'timestamp', value: Date.now().toString(), type: 'uint64' }, { name: 'username', value: 'Victory', type: 'string' }, { name: 'bio', value: 'Blockchain Developer', type: 'string' }, { name: 'owner', value: '0xYourWalletAddress', type: 'address' }, ]) await sdk.streams.set([ { id: dataId, schemaId, data: encodedData } ]) ``` Think of a Data ID like a primary key in a SQL table. | Data ID (Primary Key) | username | bio | | --------------------- | -------- | -------------------- | | 0x1234abcd... | Emmanuel | Blockchain Developer | If you write another record with the same Data ID, it updates the existing entry rather than duplicating it, thereby maintaining data integrity. `schemaId` defines how to encode/decode the data, and `dataId` identifies which record this is. The data itself is encoded and written to the blockchain ## What Are Publishers? A Publisher is any wallet address that sends data to Somnia Streams. Each publisher maintains its own isolated namespace for all schema-based data it writes. This means: * Data from two different publishers never conflict. * Apps can filter or query data from a specific publisher. * Publishers serve as the data owners for all records they create. #### Example: Getting a Publisher Address If you’re using a connected wallet, your publisher is automatically derived using the `createWalletClient` from viem: ```typescript const { ... createWalletClient, } = require("viem"); const { privateKeyToAccount } = require("viem/accounts"); // Create wallet client const walletClient = createWalletClient({ account: privateKeyToAccount(process.env.PRIVATE_KEY), chain: dreamChain, transport: http(dreamChain.rpcUrls.default.http[0]), }); // Initialize SDK const sdk = new SDK({ ... wallet: walletClient, }); const encodedData = schemaEncoder.encodeData([ ... { name: "sender", value: wallet.account.address, type: "address" }, ]); ``` Where `publisher = wallet.account.address` When reading data, you can specify which publisher’s records to fetch: ```typescript const messages = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress) ``` Example Output: ```bash [ { timestamp: 1729302920, username: "Victory", bio: "Blockchain Developer" } ] ``` This retrieves all data published under that schema by that particular address. Think of Publishers like individual database owners. Each one maintains their own “tables” (schemas) and “records” (data entries) under their unique namespace. | Publisher (Wallet) | Schema ID | Data ID | Description | | ------------------ | --------- | ------- | ---------------- | | 0x123...abc | Schema A | Data 1 | Paul’s todos | | 0x789...def | Schema A | Data 2 | Emmanuel’s todos | ## Putting It All Together When you publish data on Somnia, three identifiers always work together: | Concept | Role | Example | | --------- | ---------------------- | --------------- | | Schema ID | Identifies schema hash | 0x5e4bce54... | | Data ID | Identifies record | 0x75736572... | | Publisher | Identifies sender | 0x3dC360e038... | These three make your data verifiable, queryable, and uniquely name-spaced across the blockchain. These form the foundation of the Somnia Data Streams architecture: * The Schema tells the system what kind of data this is. * The Schema ID ensures it’s stored consistently across the network. * The Data ID identifies which record this is. * The Publisher records who wrote it. ## Example Use Case: Chat Messages Here’s how they interact in a real-world scenario, a decentralized chat room. ### Step 1: Define Schema ```typescript const chatSchema = ` uint64 timestamp, bytes32 roomId, string content, string senderName, address sender ` ``` ### Step 2: Compute Schema ID ```typescript const schemaId = await sdk.streams.computeSchemaId(chatSchema) ``` ### Step 3: Generate Data ID for each message ```typescript const dataId = toHex(`${roomName}-${Date.now()}`, { size: 32 }) ``` ### Step 4: Publish Message ```typescript const encoded = encoder.encodeData([ { name: 'timestamp', value: Date.now().toString(), type: 'uint64' }, { name: 'roomId', value: toHex(roomName, { size: 32 }), type: 'bytes32' }, { name: 'content', value: 'Hello world!', type: 'string' }, { name: 'senderName', value: 'Victory', type: 'string' }, { name: 'sender', value: publisherAddress, type: 'address' } ]) await sdk.streams.set([{ id: dataId, schemaId, data: encoded }]) ``` Now each message: * Conforms to a schema * Is identified by a Schema ID * Is stored under a unique Data ID * Is published by a specific Publisher ### Common Pitfalls | Mistake | Description | Fix | | ----------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Reusing Data IDs incorrectly | Causes overwrites of older records | Use unique IDs like title-timestamp | | Forgetting to register schema | Data won’t decode properly | Always call registerDataSchemas() once | | Mixing publisher data | Leads to incomplete reads | Query by the correct publisher address and consider aggregating many publishers under a single contract address | ### Conclusion Now that you understand Schemas, Data IDs, and Publishers, you’re ready to build your own data model for decentralized apps and query live data across multiple publishers # Extending and composing data schemas The best blockchain primitives are composable and schemas are no exception. Promoting re-use is a priority New schemas can extend other schemas by setting a parent schema ID. Remember, you can take any raw schema string and compute a schema ID from it. When registering a new schema that builds upon and extends another, you would specify the raw schema string for the new schema as well as specifying the optional parent schema ID. The parent schema ID will be critical later for deserialising data written to chain.\ \ For schemas that do not extend other schemas (when nothing is available), then one does not need to specify a parent schema ID or can optionally specify the zero value for the bytes32 solidity type.\ \ For maximum composability, all schemas should be public. ### Extension in practice (Example 1) <pre class="language-typescript"><code class="lang-typescript"><strong>import { SDK } from "@somnia-chain/streams" </strong><strong>const sdk = new SDK({ </strong> public: getPublicClient(), wallet: getWalletClient(), }) // The parent schema here will be the GPS schema from the quick start guide const gpsSchema = `uint64 timestamp, int32 latitude, int32 longitude, int32 altitude, uint32 accuracy, bytes32 entityId, uint256 nonce` const parentSchemaId = await sdk.streams.computeSchemaId(gpsSchema) // Lets extend the gps schema and add F1 data since every car will have a gps position const formulaOneSchema = `uint256 driverNumber` // We can also extend the gps schema for FR data i.e. aircraft identifier const flightRadarSchema = `bytes32 ICAO24` await sdk.streams.registerDataSchemas([ { schemaName: "gps", schema: gpsSchema }, { schemaName: "f1", schema: formulaOneSchema, parentSchemaId }, // F1 extends GPS { schemaName: "FR", schema: flightRadarSchema, parentSchemaId },// FR extends GPS ]) </code></pre> The typescript code shows how two new schemas re-use the GPS schema in order to append an additional field ### Extension in practice (Example 2) Versioned schemas ```typescript import { SDK } from "@somnia-chain/streams" const sdk = new SDK({ public: getPublicClient(), wallet: getWalletClient(), }) const versionSchema = `uint16 version` const parentSchemaId = await sdk.streams.computeSchemaId(versionSchema) // Now lets register a person schema with expectation there will be many versions of the person schema const personSchema = `uint8 age` await sdk.streams.registerDataSchemas([ { schemaName: "version", schema: versionSchema }, { schemaName: "person", schema: personSchema, parentSchemaId } ]) ``` Client's that are reading data associated with the derived schemas, use the SDK to get the fully decoded data since data is retrieved by schema ID (See `getByKey` from the quick start guide). Essentially the SDK does a number of the following pseudo steps: 1. Fetch schema and recursively fetch parent schema until the end of the chain is reached 2. Join all schemas together seperated by comma 3. Spin up the decoder and pass through the raw data stored on-chain 4. Return the decoded data to the caller # Somnia Data vs Event Streams Serving different purposes, data and event streams can be used independently or together ### tl;dr * Data Streams: Raw bytes calldata written to chain with contextual information on how to parse the data using a public or private `data schema` * Event Streams: [EVM logs](https://docs.chainstack.com/docs/ethereum-logs-tutorial-series-logs-and-filters) emitted by the Somnia Streams protocol. Protocol users register and `event schema` that can be referenced they want to emit an event that others can `subscribe` to with Somnia streams reactivity Both data and event streams can be done without knowing Solidity and without deploying any smart contracts ### TypeScript SDK interface ```typescript /** * @param somniaStreamsEventId The identifier of a registered event schema within Somnia streams protocol or null if using a custom event source * @param ethCalls Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array * @param context Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, topic4, data and address * @param onData Callback for a successful reactivity notification * @param onError Callback for a failed attempt * @param eventContractSource Alternative contract event source (any on somnia) that will be emitting the logs specified by topicOverrides * @param topicOverrides Optional when using Somnia streams as an event source but mandatory when using a different event source * @param onlyPushChanges Whether the data should be pushed to the subscriber only if eth_call results are different from the previous */ export type SubscriptionInitParams = { somniaStreamsEventId?: string ethCalls: EthCall[] context?: string onData: (data: any) => void onError?: (error: Error) => void eventContractSource?: Address topicOverrides?: Hex[] onlyPushChanges: boolean } export interface StreamsInterface { // Write set(d: DataStream[]): Promise<Hex | null>; emitEvents(e: EventStream[]): Promise<Hex | Error | null>; setAndEmitEvents(d: DataStream[], e: EventStream[]): Promise<Hex | Error | null>; // Manage registerDataSchemas(registrations: DataSchemaRegistration[]): Promise<Hex | Error | null>; registerEventSchemas(ids: string[], schemas: EventSchema[]): Promise<Hex | Error | null>; manageEventEmittersForRegisteredStreamsEvent( streamsEventId: string, emitter: Address, isEmitter: boolean ): Promise<Hex | Error | null>; // Read getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>; getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>; getBetweenRange( schemaId: SchemaID, publisher: Address, startIndex: bigint, endIndex: bigint ): Promise<Hex[] | SchemaDecodedItem[][] | Error | null>; getAllPublisherDataForSchema( schemaReference: SchemaReference, publisher: Address ): Promise<Hex[] | SchemaDecodedItem[][] | null>; getLastPublishedDataForSchema( schemaId: SchemaID, publisher: Address ): Promise<Hex[] | SchemaDecodedItem[][] | null>; totalPublisherDataForSchema(schemaId: SchemaID, publisher: Address): Promise<bigint | null>; isDataSchemaRegistered(schemaId: SchemaID): Promise<boolean | null>; computeSchemaId(schema: string): Promise<Hex | null>; parentSchemaId(schemaId: SchemaID): Promise<Hex | null>; schemaIdToId(schemaId: SchemaID): Promise<string | null>; idToSchemaId(id: string): Promise<Hex | null>; getAllSchemas(): Promise<string[] | null>; getEventSchemasById(ids: string[]): Promise<EventSchema[] | null>; // Helper deserialiseRawData( rawData: Hex[], parentSchemaId: Hex, schemaLookup: { schema: string; schemaId: Hex; } | null ): Promise<Hex[] | SchemaDecodedItem[][] | null>; // Subscribe subscribe(initParams: SubscriptionInitParams): Promise<{ subscriptionId: string, unsubscribe: () => void } | undefined>; // Protocol getSomniaDataStreamsProtocolInfo(): Promise<GetSomniaDataStreamsProtocolInfoResponse | Error | null>; } ``` # Intersection with Somnia Reactivity How to build applications that react to data being streamed to the Somnia chain by creating subscriptions ## Reactivity background For detailed information about reactivity please visit the Reactivity docs: <table data-view="cards"><thead><tr><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Reactivity</td><td><a href="/developer/reactivity#welcome-to-the-somnia-reactivity-docs">Somnia Reactivity</a></td></tr></tbody></table> ## Writing data, events and reacting When an ERC20 transfer takes place, balance state is updated and an event is emitted. The ERC20 transfer scenario is very common in smart contracts i.e. publishing state and emitting an event (also known as a log). Somnia Data Streams offers you the tooling to do this without the requirements of having to write your own custom Solidity contract. It also allows you to take advantage of existing schemas for publishing data yielding composibility benefits for applications.\ \ Example: ```typescript import { SDK } from "@somnia-chain/streams" import { zeroAddress, erc721Abi } from "viem" // Use WebSocket transport in the public client for subscription tasks // For the SDK instance that executes transactions, stick with htttp const sdk = new SDK({ public: getPublicClient(), wallet: getWalletClient(), }) // Encode view function calls to be executed when an event takes place const ethCalls = [{ to: "0x23B66B772AE29708a884cca2f9dec0e0c278bA2c", data: encodeFunctionData({ abi: erc721Abi, functionName: "balanceOf", args: ["0x3dC360e0389683cA0341a11Fc3bC26252b5AF9bA"] }) }] // Start a subsciption const subscription = await sdk.streams.subscribe({ ethCalls, onData: (data) => { const decodedLog = decodeEventLog({ abi: fireworkABI, topics: data.result.topics, data: data.result.data, }); const decodedFunctionResult = decodeFunctionResult({ abi: erc721Abi, functionName: 'balanceOf', data: data.result.simulationResults[0], }); console.log("Decoded event", decodedLog); console.log("Decoded function call result", decodedFunctionResult); } }) // Write data and emit events that will trigger the above callback! const dataStreams = [{ id, schemaId: driverSchemaId, data: encodedData }] const eventStreams = [{ id: somniaStreamsEventId, argumentTopics, data }] const setAndEmitEventsTxHash = await sdk.streams.setAndEmitEvents( dataStreams, eventStreams ) ``` Writing data and emitting events will trigger a call back to subscribers that care about a specified event emitted from the Somnia Data Streams protocol (or any contract for that matter) without having the need to poll the chain. It follows the observer pattern meaning push rather than pull which is always a more efficient paradigm. # Data Provenance and Verification in Streams When consuming data from any source, especially in a decentralized environment, the most critical question is: **"Can I trust this data?"** This question is not just about the data's content, but its *origin*. How do you know that data claiming to be from a trusted oracle, a specific device, or another user *actually* came from them and not from an imposter? This is the challenge of **Data Provenance**. In Somnia Data Streams, provenance is not an optional feature or a "best practice". It is a fundamental, cryptographic guarantee built into the core smart contract. This article explains how Streams ensures authenticity via publisher signatures and how you can verify data origin. ## The Cryptographic Guarantee: `msg.sender` as Provenance The trust layer of Somnia Streams is elegantly simple. It does not rely on complex off-chain signature checking or data fields like `senderName`. Instead, it leverages the most basic and secure primitive of the EVM: `msg.sender`. All data published to Streams is stored in the core `Streams` smart contract. The data storage mapping has a specific structure: #### **Conceptual Contract Storage** ```solidity // mapping: schemaId => publisherAddress => dataId => data mapping(bytes32 => mapping(address => mapping(bytes32 => bytes))) public dsstore; ``` When a publisher calls `sdk.streams.set(...)` or `sdk.streams.setAndEmitEvents(...)`, their wallet signs a transaction. The `Streams` smart contract receives this transaction and identifies the signer's address via the `msg.sender` variable. The contract then stores the data *at the `msg.sender`'s address* within the schema's mapping. **This is the cryptographic guarantee.** It is **impossible** for `0xPublisher_A` to send a transaction that writes data into the slot for `0xPublisher_B`. They cannot fake their `msg.sender`. The data is automatically and immutably tied to the address of the account that paid the gas to publish it. * An attacker **cannot** write data as if it came from a trusted oracle. * A user **cannot** send a chat message pretending to be another user. * Data integrity is linked directly to wallet security. ## Verification Is Implicit in the Read Operation Because the `publisher` address is a fundamental key in the storage mapping, you don't need to perform complex "verification" steps. **Verification is implicit in the read operation.** When you use the SDK to read data, you must specify which publisher you are interested in: * `sdk.streams.getByKey(schemaId, publisher, key)` * `sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)` When you call `getAllPublisherDataForSchema(schemaId, '0xTRUSTED_ORACLE_ADDRESS')`, you are not *filtering* data. You are asking the smart contract to retrieve data from the specific storage slot that *only* `0xTRUSTED_ORACLE_ADDRESS` could have written to. If an imposter (`0xIMPOSTER_ADDRESS`) publishes data using the same `schemaId`, their data is stored in a completely different location (`dsstore[schemaId]['0xIMPOSTER_ADDRESS']`). It will never be returned when you query for the trusted address. ## Deliverable: Building a Verification Script Let's build a utility to prove this concept. **Scenario:** We have a shared `oraclePrice` schema. Two different, trusted oracles (`0xOracle_A` and `0xOracle_B`) publish prices to it. We will build a script that verifies the origin of data and proves that an `imposter` cannot pollute their feeds. ### **Project Setup** We will use the same project setup as the "[Multi-Publisher Aggregator](https://emre-gitbook.gitbook.io/emre-gitbook-d
프로젝트로 돌아가기