Veřejný test: Řešení pro ochranu soukromí a škálovatelnost na Ethereu

Blockchain je inovativní technologie, která slibuje zlepšení mnoha oblastí lidského života. Přenáší reálné procesy a produkty do digitálního prostoru, zajišťuje rychlost a spolehlivost finančních transakcí, snižuje jejich náklady a také umožňuje vytvářet moderní aplikace DAPP pomocí smart kontraktů v decentralizovaných sítích.

Vzhledem k mnoha výhodám a různorodým aplikacím blockchainu se může zdát překvapivé, že se tato slibná technologie ještě nedostala do každého odvětví. Problém je v tom, že moderním decentralizovaným blockchainům chybí škálovatelnost. Ethereum zpracovává asi 20 transakcí za sekundu, což nestačí k uspokojení potřeb dnešních dynamických podniků. Společnosti využívající technologii blockchain zároveň váhají s opuštěním Etherea kvůli jeho vysokému stupni ochrany před hackery a výpadky sítě.

Aby byla zajištěna decentralizace, bezpečnost a škálovatelnost v blockchainu, čímž se řeší Trilemma škálovatelnosti, vývojový tým Opporty vytvořil Plasma Cash, dceřiný řetězec sestávající z chytré smlouvy a privátní sítě založené na Node.js, která pravidelně přenáší svůj stav do kořenového řetězce (Ethereum).

Veřejný test: Řešení pro ochranu soukromí a škálovatelnost na Ethereu

Klíčové procesy v Plasma Cash

1. Uživatel zavolá funkci chytré smlouvy `vklad` a předá do ní množství ETH, které chce vložit do tokenu Plasma Cash. Funkce inteligentní smlouvy vytvoří token a vygeneruje o něm událost.

2. Plasma Cash uzly přihlášené k událostem smart contract obdrží událost o vytvoření vkladu a přidají transakci o vytvoření tokenu do fondu.

3. Speciální uzly Plasma Cash pravidelně odebírají všechny transakce z fondu (až do 1 milionu) a tvoří z nich blok, vypočítají Merkle strom a podle toho hash. Tento blok je odeslán do jiných uzlů k ověření. Uzly kontrolují, zda je Merkle hash platný a zda jsou transakce platné (například zda odesílatel tokenu je jeho vlastníkem). Po ověření bloku zavolá uzel funkci `submitBlock` chytré smlouvy, která uloží číslo bloku a Merkle hash do řetězce hran. Inteligentní smlouva generuje událost indikující úspěšné přidání bloku. Transakce jsou z fondu odstraněny.

4. Uzly, které obdrží událost odeslání bloku, začnou aplikovat transakce, které byly přidány do bloku.

5. V určitém okamžiku jej vlastník (nebo nevlastník) tokenu chce stáhnout z Plasma Cash. K tomu zavolá funkci `startExit` a předá do ní informace o posledních 2 transakcích na tokenu, které potvrzují, že je vlastníkem tokenu. Chytrý kontrakt pomocí Merkle hashe zkontroluje přítomnost transakcí v blocích a odešle token k výběru, ke kterému dojde za dva týdny.

6. Pokud k operaci výběru tokenu došlo s porušením (token byl utracen po zahájení postupu výběru nebo token již před výběrem patřil někomu jinému), vlastník tokenu může výběr vyvrátit do dvou týdnů.

Veřejný test: Řešení pro ochranu soukromí a škálovatelnost na Ethereu

Soukromí je dosaženo dvěma způsoby

1. Kořenový řetězec neví nic o transakcích, které jsou generovány a předávány v rámci podřízeného řetězce. Informace o tom, kdo vložil a vybral ETH z Plasma Cash, zůstávají veřejné.

2. Podřízený řetězec umožňuje anonymní transakce pomocí zk-SNARK.

Zásobník technologií

  • NodeJS
  • Redestilát
  • Etherium
  • zašpiněné

Testování

Při vývoji Plasma Cash jsme testovali rychlost systému a získali následující výsledky:

  • do fondu je přidáno až 35 000 transakcí za sekundu;
  • do bloku lze uložit až 1 000 000 transakcí.

Testy byly provedeny na následujících 3 serverech:

1. Intel Core i7-6700 Quad-Core Skylake vč. NVMe SSD – 512 GB, 64 GB DDR4 RAM
Byly navýšeny 3 validující Plasma Cash uzly.

2. AMD Ryzen 7 1700X Octa-Core „Summit Ridge“ (Zen), SATA SSD – 500 GB, 64 GB DDR4 RAM
Byl zvednut uzel Ropsten testnet ETH.
Byly navýšeny 3 validující Plasma Cash uzly.

3. Intel Core i9-9900K Octa-Core vč. NVMe SSD – 1 TB, 64 GB DDR4 RAM
Byl zvednut 1 uzel pro odevzdání plazmové hotovosti.
Byly navýšeny 3 validující Plasma Cash uzly.
Byl spuštěn test přidání transakcí do sítě Plasma Cash.

Celkem: 10 Plasma Cash uzlů v privátní síti.

Test 1

Limit je 1 milion transakcí na blok. 1 milion transakcí tedy spadá do 2 bloků (protože systém stihne část transakcí převzít a odeslat již při jejich odesílání).


Počáteční stav: poslední blok #7; V databázi je uloženo 1 milion transakcí a tokenů.

00:00 — začátek skriptu generování transakce
01:37 - Byl vytvořen 1 milion transakcí a začalo odesílání do uzlu
01:46 — uzel odeslání převzal 240 8 transakcí z bloku a bloku #320 formulářů. Také vidíme, že 10 XNUMX transakcí je přidáno do fondu za XNUMX sekund
01:58 — blok #8 je podepsán a odeslán k ověření
02:03 — blok #8 je ověřen a funkce `submitBlock` chytré smlouvy je volána s Merkle hashem a číslem bloku
02:10 — skončil funkční demo skript, který odeslal 1 milion transakcí za 32 sekund
02:33 - uzly začaly přijímat informace, že blok #8 byl přidán do kořenového řetězce, a začaly provádět 240 XNUMX transakcí
02:40 - Z fondu bylo odstraněno 240 8 transakcí, které jsou již v bloku #XNUMX
02:56 — Submit node vzal zbývajících 760 9 transakcí z fondu a začal vypočítávat Merkle hash a blok podpisu #XNUMX
03:20 - všechny uzly obsahují 1 milion 240 XNUMX transakcí a tokenů
03:35 — blok #9 je podepsán a odeslán k ověření dalším uzlům
03:41 - došlo k chybě sítě
04:40 — čekání na ověření bloku #9 vypršel
04:54 — Submit node vzal zbývajících 760 9 transakcí z fondu a začal vypočítávat Merkle hash a blok podpisu #XNUMX
05:32 — blok #9 je podepsán a odeslán k ověření dalším uzlům
05:53 — blok #9 je ověřen a odeslán do kořenového řetězce
06:17 - uzly začaly přijímat informace, že blok #9 byl přidán do kořenového řetězce a začaly provádět 760 tisíc transakcí
06:47 — fond se vyčistil od transakcí, které jsou v bloku #9
09:06 - všechny uzly obsahují 2 miliony transakcí a tokenů

Test 2

Limit je 350 tisíc na blok. V důsledku toho máme 3 bloky.


Počáteční stav: poslední blok #9; V databázi jsou uloženy 2 miliony transakcí a tokenů

00:00 — skript generování transakcí již byl spuštěn
00:44 - Byl vytvořen 1 milion transakcí a začalo odesílání do uzlu
00:56 — uzel odeslání převzal 320 10 transakcí z bloku a bloku #320 formulářů. Také vidíme, že 10 XNUMX transakcí je přidáno do fondu za XNUMX sekund
01:12 — blok #10 je podepsán a odeslán do jiných uzlů k ověření
01:18 — skončil funkční demo skript, který odeslal 1 milion transakcí za 34 sekund
01:20 — blok #10 je ověřen a odeslán do kořenového řetězce
01:51 - všechny uzly obdržely informace z kořenového řetězce, že byl přidán blok #10, a začnou uplatňovat 320 XNUMX transakcí
02:01 - fond se vyčistil pro 320 tisíc transakcí, které byly přidány do bloku #10
02:15 — uzel odeslání převzal 350 11 transakcí z bloku #XNUMX fondu a formulářů
02:34 — blok #11 je podepsán a odeslán do jiných uzlů k ověření
02:51 — blok #11 je ověřen a odeslán do kořenového řetězce
02:55 — poslední uzel dokončil transakce z bloku #10
10:59 — transakce s odesláním bloku #9 trvala v kořenovém řetězci velmi dlouho, ale byla dokončena a všechny uzly o ní dostaly informace a začaly provádět 350 tisíc transakcí
11:05 - fond se vyčistil pro 320 tisíc transakcí, které byly přidány do bloku #11
12:10 - všechny uzly obsahují 1 milion 670 XNUMX transakcí a tokenů
12:17 — uzel odeslání převzal 330 12 transakcí z bloku #XNUMX fondu a formulářů
12:32 — blok #12 je podepsán a odeslán do jiných uzlů k ověření
12:39 — blok #12 je ověřen a odeslán do kořenového řetězce
13:44 - všechny uzly obdržely informace z kořenového řetězce, že byl přidán blok #12, a začnou uplatňovat 330 XNUMX transakcí
14:50 - všechny uzly obsahují 2 miliony transakcí a tokenů

Test 3

Na prvním a druhém serveru byl jeden ověřovací uzel nahrazen odesílajícím uzlem.


Počáteční stav: poslední blok #84; 0 transakcí a tokenů uložených v databázi

00:00 — Byly spuštěny 3 skripty, které generují a odesílají každý 1 milion transakcí
01:38 — Bylo vytvořeno 1 milion transakcí a bylo zahájeno odesílání do uzlu č. 3 pro odeslání
01:50 — uzel odeslání #3 vzal 330 85 transakcí z fondu a bloku formulářů #21 (f350). Také vidíme, že 10 XNUMX transakcí je přidáno do fondu za XNUMX sekund
01:53 — Bylo vytvořeno 1 milion transakcí a bylo zahájeno odesílání do uzlu č. 1 pro odeslání
01:50 — uzel odeslání #3 vzal 330 85 transakcí z fondu a bloku formulářů #21 (f350). Také vidíme, že 10 XNUMX transakcí je přidáno do fondu za XNUMX sekund
02:01 — Odeslat uzel #1 vzal 250 85 transakcí z fondu a bloku formulářů #65 (XNUMXe)
02:06 — blok #85 (f21) je podepsán a odeslán do jiných uzlů k ověření
02:08 — demo skript serveru #3, který odeslal 1 milion transakcí za 30 sekund, skončil
02:14 — blok #85 (f21) je ověřen a odeslán do kořenového řetězce
02:19 — blok #85 (65e) je podepsán a odeslán do jiných uzlů k ověření
02:22 — Bylo vytvořeno 1 milion transakcí a bylo zahájeno odesílání do uzlu č. 2 pro odeslání
02:27 — blok #85 (65e) ověřen a odeslán do kořenového řetězce
02:29 — uzel odeslání č. 2 převzal 111855 85 transakcí z bloku a bloku formulářů č. 256 (XNUMX).
02:36 — blok #85 (256) je podepsán a odeslán do jiných uzlů k ověření
02:36 — demo skript serveru #1, který odeslal 1 milion transakcí za 42.5 sekund, skončil
02:38 — blok #85 (256) je ověřen a odeslán do kořenového řetězce
03:08 — Skript serveru #2 dokončil práci, která odeslala 1 milion transakcí za 47 sekund
03:38 - všechny uzly obdržely informace z kořenového řetězce, že byly přidány bloky #85 (f21), #86(65e), #87(256) a začaly uplatňovat 330k, 250k, 111855 transakcí
03:49 - fond byl vymazán při 330 250, 111855 85, 21 86 transakcích, které byly přidány do bloků #65 (f87), #256(XNUMXe), #XNUMX(XNUMX)
03:59 — uzel odeslání #1 vzal 888145 88 transakcí z bloku a bloku formulářů #214 (2), uzel odeslání #750 vzal 88 tisíc transakcí z fondu a blok formulářů #50 (3a), uzel odeslání #670 vzal 88 tisíc transakcí z blok a blok #3 (dXNUMXb)
04:44 — blok #88 (d3b) je podepsán a odeslán do jiných uzlů k ověření
04:58 — blok #88 (214) je podepsán a odeslán do jiných uzlů k ověření
05:11 — blok #88 (50a) je podepsán a odeslán do ostatních uzlů k ověření
05:11 — blok #85 (d3b) je ověřen a odeslán do kořenového řetězce
05:36 — blok #85 (214) je ověřen a odeslán do kořenového řetězce
05:43 - všechny uzly obdržely informace z kořenového řetězce, že byly přidány bloky #88 (d3b), #89(214) a začínají uplatňovat 670k, 750k transakcí
06:50 — z důvodu selhání komunikace nebyl blok #85 (50a) ověřen
06:55 — uzel odeslání č. 2 převzal 888145 90 transakcí z bloku a bloku formulářů č. 50 (XNUMXa)
08:14 — blok #90 (50a) je podepsán a odeslán do ostatních uzlů k ověření
09:04 — blok #90 (50a) je ověřen a odeslán do kořenového řetězce
11:23 - všechny uzly obdržely informace z kořenového řetězce, že byl přidán blok #90 (50a), a začnou uplatňovat 888145 transakcí. Současně server #3 již použil transakce z bloků #88 (d3b), #89(214)
12:11 - všechny bazény jsou prázdné
13:41 — všechny uzly serveru #3 obsahují 3 miliony transakcí a tokenů
14:35 — všechny uzly serveru #1 obsahují 3 miliony transakcí a tokenů
19:24 — všechny uzly serveru #2 obsahují 3 miliony transakcí a tokenů

Překážky

Při vývoji Plasma Cash jsme narazili na následující problémy, které jsme postupně řešili a řešíme:

1. Konflikt v interakci různých funkcí systému. Například funkce přidávání transakcí do fondu blokovala práci při odesílání a ověřování bloků a naopak, což vedlo k poklesu rychlosti.

2. Nebylo hned jasné, jak odeslat obrovské množství transakcí a zároveň minimalizovat náklady na přenos dat.

3. Nebylo jasné, jak a kam data ukládat, aby bylo dosaženo vysokých výsledků.

4. Nebylo jasné, jak organizovat síť mezi uzly, protože velikost bloku s 1 milionem transakcí zabírá asi 100 MB.

5. Práce v jednovláknovém režimu přeruší spojení mezi uzly, když dojde k dlouhým výpočtům (například sestavení Merkleho stromu a výpočet jeho hash).

Jak jsme se s tím vším vypořádali?

První verze uzlu Plasma Cash byla jakýmsi kombajnem, který mohl dělat všechno současně: přijímat transakce, odesílat a ověřovat bloky a poskytovat API pro přístup k datům. Protože NodeJS je nativně jednovláknový, funkce výpočtu stromu Merkle zablokovala funkci přidání transakce. Viděli jsme dvě možnosti řešení tohoto problému:

1. Spusťte několik procesů NodeJS, z nichž každý vykonává specifické funkce.

2. Použijte worker_threads a přesuňte provádění části kódu do vláken.

V důsledku toho jsme použili obě možnosti současně: jeden uzel jsme logicky rozdělili na 3 části, které mohou fungovat samostatně, ale zároveň synchronně

1. Uzel odeslání, který přijímá transakce do fondu a vytváří bloky.

2. Ověřovací uzel, který kontroluje platnost uzlů.

3. API uzel – poskytuje API pro přístup k datům.

V tomto případě se můžete ke každému uzlu připojit přes unixovou zásuvku pomocí cli.

Těžké operace, jako je výpočet Merkleho stromu, jsme přesunuli do samostatného vlákna.

Tím jsme dosáhli normálního fungování všech funkcí Plasma Cash současně a bez poruch.

Jakmile byl systém funkční, začali jsme testovat rychlost a bohužel jsme dostali neuspokojivé výsledky: 5 000 transakcí za sekundu a až 50 000 transakcí na blok. Musel jsem přijít na to, co bylo špatně implementováno.

Nejprve jsme začali testovat mechanismus komunikace s Plasma Cash, abychom zjistili špičkovou kapacitu systému. Již dříve jsme psali, že uzel Plasma Cash poskytuje rozhraní soketu unix. Zpočátku to bylo založeno na textu. Objekty json byly odeslány pomocí `JSON.parse()` a `JSON.stringify()`.

```json
{
  "action": "sendTransaction",
  "payload":{
    "prevHash": "0x8a88cc4217745fd0b4eb161f6923235da10593be66b841d47da86b9cd95d93e0",
    "prevBlock": 41,
    "tokenId": "57570139642005649136210751546585740989890521125187435281313126554130572876445",
    "newOwner": "0x200eabe5b26e547446ae5821622892291632d4f4",
    "type": "pay",
    "data": "",
    "signature": "0xd1107d0c6df15e01e168e631a386363c72206cb75b233f8f3cf883134854967e1cd9b3306cc5c0ce58f0a7397ae9b2487501b56695fe3a3c90ec0f61c7ea4a721c"
  }
}
```

Změřili jsme přenosovou rychlost takových objektů a zjistili jsme ~ 130k za sekundu. Zkusili jsme nahradit standardní funkce pro práci s json, ale výkon se nezlepšil. Motor V8 musí být pro tyto operace dobře optimalizován.

Pracovali jsme s transakcemi, tokeny a bloky prostřednictvím tříd. Při vytváření takových tříd klesl výkon 2krát, což naznačuje, že OOP pro nás není vhodný. Musel jsem vše přepsat na čistě funkční přístup.

Záznam do databáze

Zpočátku byl Redis vybrán pro ukládání dat jako jedno z nejproduktivnějších řešení, které splňuje naše požadavky: úložiště klíč-hodnota, práce s hashovacími tabulkami, sadami. Spustili jsme redis-benchmark a získali ~ 80 1 operací za sekundu v XNUMX pipelining režimu.

Pro vysoký výkon jsme Redis vyladili jemněji:

  • Bylo vytvořeno připojení unixového soketu.
  • Zakázali jsme ukládání stavu na disk (kvůli spolehlivosti můžete nastavit repliku a uložit na disk v samostatném Redis).

V Redis je fond hashovací tabulkou, protože musíme být schopni načíst všechny transakce v jednom dotazu a mazat transakce jednu po druhé. Zkoušeli jsme použít běžný seznam, ale při načítání celého seznamu je to pomalejší.

Při použití standardního NodeJS dosáhly knihovny Redis výkonu 18 tisíc transakcí za sekundu. Rychlost klesla 9krát.

Vzhledem k tomu, že nám benchmark ukázal, že možnosti byly jasně 5x větší, začali jsme s optimalizací. Změnili jsme knihovnu na ioredis a dostali jsme výkon 25k za sekundu. Transakce jsme přidávali jednu po druhé pomocí příkazu `hset`. Takže jsme v Redis generovali spoustu dotazů. Vznikl nápad spojit transakce do dávek a odeslat je jedním příkazem `hmset`. Výsledkem je 32 tisíc za sekundu.

Z několika důvodů, které popíšeme níže, pracujeme s daty pomocí `Buffer` a jak se ukázalo, pokud je před zápisem převedete na text (`buffer.toString('hex')`), můžete získat další výkon. Rychlost se tak zvýšila na 35k za sekundu. V tuto chvíli jsme se rozhodli pozastavit další optimalizaci.

Museli jsme přejít na binární protokol, protože:

1. Systém často vypočítává hashe, signatury atd., a k tomu potřebuje data v `Bufferu.

2. Při odesílání mezi službami váží binární data méně než text. Například při odeslání bloku s 1 milionem transakcí mohou data v textu zabrat více než 300 megabajtů.

3. Neustálá transformace dat ovlivňuje výkon.

Proto jsme vzali za základ vlastní binární protokol pro ukládání a přenos dat, vyvinutý na základě úžasné knihovny „binárních dat“.

V důsledku toho jsme získali následující datové struktury:

-Transakce

  ```json
  {
    prevHash: BD.types.buffer(20),
    prevBlock: BD.types.uint24le,
    tokenId: BD.types.string(null),
    type: BD.types.uint8,
    newOwner: BD.types.buffer(20),
    dataLength: BD.types.uint24le,
    data: BD.types.buffer(({current}) => current.dataLength),
    signature: BD.types.buffer(65),
    hash: BD.types.buffer(32),
    blockNumber: BD.types.uint24le,
    timestamp: BD.types.uint48le,
  }
  ```

— Token

  ```json
  {
    id: BD.types.string(null),
    owner: BD.types.buffer(20),
    block: BD.types.uint24le,
    amount: BD.types.string(null),
  }
  ```

-Blok

  ```json
  {
    number: BD.types.uint24le,
    merkleRootHash: BD.types.buffer(32),
    signature: BD.types.buffer(65),
    countTx: BD.types.uint24le,
    transactions: BD.types.array(Transaction.Protocol, ({current}) => current.countTx),
    timestamp: BD.types.uint48le,
  }
  ```

Pomocí obvyklých příkazů `BD.encode(block, Protocol).slice();` a `BD.decode(buffer, Protocol)` převádíme data na `Buffer` pro uložení v Redis nebo předání do jiného uzlu a načtení data zpět.

Máme také 2 binární protokoly pro přenos dat mezi službami:

— Protokol pro interakci s Plasma Node přes unixovou zásuvku

  ```json
  {
    type: BD.types.uint8,
    messageId: BD.types.uint24le,
    error: BD.types.uint8,
    length: BD.types.uint24le,
    payload: BD.types.buffer(({node}) => node.length)
  }
  ```

kde:

  • `Typ` — akce, která má být provedena, například 1 — sendTransaction, 2 — getTransaction;
  • "užitné zatížení". — data, která je třeba předat příslušné funkci;
  • "ID zprávy". — ID zprávy, aby bylo možné identifikovat odpověď.

— Protokol pro interakci mezi uzly

  ```json
  {
    code: BD.types.uint8,
    versionProtocol: BD.types.uint24le,
    seq: BD.types.uint8,
    countChunk: BD.types.uint24le,
    chunkNumber: BD.types.uint24le,
    length: BD.types.uint24le,
    payload: BD.types.buffer(({node}) => node.length)
  }
  ```

kde:

  • "kód". — kód zprávy, například 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — verze protokolu, protože v síti mohou být vytvořeny uzly s různými verzemi a mohou pracovat odlišně;
  • "seq". — identifikátor zprávy;
  • `countChunk` и `chunkNumber` nezbytné pro rozdělení velkých zpráv;
  • "délka". и "užitné zatížení". délka a samotná data.

Protože jsme data předem zadali, konečný systém je mnohem rychlejší než knihovna `rlp` Etherea. Bohužel se nám to zatím nepodařilo odmítnout, protože je potřeba dokončit smart kontrakt, což plánujeme do budoucna.

Pokud se nám podařilo dosáhnout rychlosti 35 000 transakcí za sekundu, musíme je také zpracovat v optimálním čase. Protože přibližná doba vytvoření bloku trvá 30 sekund, musíme do bloku zahrnout 1 000 000 transakcí, což znamená poslat více 100 MB dat.

Zpočátku jsme ke komunikaci mezi uzly používali knihovnu `ethereumjs-devp2p`, ale ta nedokázala zpracovat tolik dat. V důsledku toho jsme použili knihovnu `ws` a nakonfigurovali odesílání binárních dat přes websocket. Samozřejmě jsme také narazili na problémy při odesílání velkých datových paketů, ale rozdělili jsme je na bloky a nyní jsou tyto problémy pryč.

Také vytvoření Merkleho stromu a výpočet hashe 1 000 000 transakce vyžaduje asi 10 sekund nepřetržitého výpočtu. Během této doby se spojení se všemi uzly podaří přerušit. Bylo rozhodnuto přesunout tento výpočet do samostatného vlákna.

Závěry:

Ve skutečnosti naše poznatky nejsou nové, ale z nějakého důvodu na ně mnozí odborníci při vývoji zapomínají.

  • Použití funkcionálního programování místo objektově orientovaného programování zvyšuje produktivitu.
  • Monolit je horší než architektura služeb pro produktivní systém NodeJS.
  • Použití `worker_threads` pro náročné výpočty zlepšuje odezvu systému, zejména při práci s I/O operacemi.
  • unixový socket je stabilnější a rychlejší než požadavky http.
  • Pokud potřebujete rychle přenést velká data po síti, je lepší použít websockets a posílat binární data, rozdělená na kousky, které lze přeposlat, pokud nedorazí, a následně spojit do jedné zprávy.

Zveme vás k návštěvě GitHub projekt: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Článek byl spoluautorem Alexandr Nashivan, senior vývojář Společnost Clever Solution Inc.

Zdroj: www.habr.com

Přidat komentář