預言機 (Oracles)

在本章中,我們將討論預言機 oracles,它是可以為以太坊智能合約提供外部數據源的系統。華人區塊鏈裡翻譯 oracle 為預言機;“神諭” Oracle一詞來自希臘神話,它提到了一個與神靈交流的人,他們可以看到未來的願景。 在區塊鏈的上下文中,oracle 是一個回答問題的系統,它可以回答來自於以太坊之外問題。 理想情況下,oracles 是不可靠 (trustless) 的系統,按造中心化原則 (decentralized),這意味著它們不需要被信任。

為什麼預言機是剛需

以太坊平台中的一個關鍵元件是以太坊虛擬機,這虛擬機能夠執行程式,並且在共識算法的規則約束中,能夠在去中心化網路中的任何一節點上更新乙太坊的狀態。為了保持共識,EVM 的執行必須是完全有確定性的 (deterministic),並且該執行只能基於乙太坊狀態以及簽名交易的共享執行脈落進行。這有兩個特別重要的後果:第一個是 EVM 和智能合約沒有任何內在的隨機性來源;第二,外部之數據僅能以交易中資料封包 (data payload) 的方式來進入。

讓我們進一步分析這兩種後果。要了解在 EVM 中禁止使用真正的隨機函數為智能合約提供隨機性,請考慮執行此類功能後對達成共識的嘗試的影響:節點A將執行命令並代表智能合約存儲 3 在其存儲中,執行相同智能合約的節點B會存儲 7。因此,儘管在相同的上下文中運行了完全相同的代碼,但節點A和B將得出關於結果狀態應該是什麼的不同結論。確實,每次評估智能合約時,都有可能獲得不同的結果狀態。這樣一來,網絡由於其眾多節點在世界各地獨立運行,將無法就結果狀態應達成分散式共識。在實踐中,它將很快變得比本示例差很多,因為包括醚轉移在內的連鎖效應將成倍增加。

注意,偽隨機函數,例如加密安全散列函數(它們是確定性的,因此可以是,實際上是EVM的一部分),對於許多應用來說是不夠的。 採取模擬硬幣翻轉的賭博遊戲來解決投注支出,這需要隨機化頭部或尾部 - 礦工可以通過玩遊戲獲得優勢,並且僅包括他們將贏得的區塊中的交易。 那麼我們如何解決這個問題呢? 那麼,所有節點都可以就簽名交易的內容達成一致,因此可以引入外部信息,包括隨機性,價格信息,天氣預報等,作為發送到網絡的交易的數據部分。 但是,這些數據根本無法信任,因為它來自無法核實的來源。 因此,我們剛剛推遲了這個問題。 我們使用oracles嘗試解決這些問題,我們將在本章的其餘部分詳細討論這些問題。

預言機的使用例子 (Use Case) 及範例

理想情況下,神諭提供了一種無信任(或至少近乎無信任)的方式來獲取外在的(即“真實世界”或鏈下"off-chain")信息,例如足球比賽的結果,黃金的價格,或真正的隨機數字,在以太坊平台上使用智能合約。它們還可用於直接將數據安全地中繼到 DApp 前端。因此,可以將神諭視為彌合脫軌世界與智能合約之間差距的機制。允許智能合約基於真實世界的事件和數據來強制執行合同關係,從而大大擴展了它們的範圍。但是,這也會給以太坊的安全模型帶來外部風險。考慮一個“聰明意志”合同,當一個人去世時分配資產。這是智能合約空間中經常討論的內容,並突出了可信任的oracle的風險。如果由這樣的合同控制的繼承金額足夠高,那麼在所有者死亡之前攻擊oracle並觸發資產分配的動機是非常高的。

請注意,某些oracles提供特定於特定私有數據源的數據,例如學術證書或政府ID。 這些數據的來源,如大學或政府部門,是完全可信的,數據的真實性是主觀的(真相只能通過訴諸來源的權威來確定)。 因此,不能無信地提供這樣的數據 - 即,不信任來源 - 因為沒有獨立可驗證的客觀事實。 因此,我們將這些數據源包含在我們對“神諭”的定義中,因為它們還為智能合約提供了數據橋樑。 他們提供的數據通常採用證明的形式,如護照或成就記錄。 考試將成為未來區塊鏈平台成功的重要組成部分,特別是在驗證身份或聲譽的相關問題方面,因此探索區塊鏈平台如何為其提供服務非常重要。

可能由oracles提供的更多數據示例包括:

  • 來自物理來源的隨機數/熵(例如量子/熱現象):公平地選擇彩票智能合約中的贏家

  • 與自然災害相關的參數觸發器:觸發巨災債券智能合約(對於颶風債券來說的風速)

  • 匯率數據:將穩定幣與法定貨幣準確掛鉤

  • 資本市場數據:代幣化資產/證券的定價籃子

  • 基準參考數據:將利率納入智能金融衍生品

  • 靜態/僞靜態數據:安全識別碼,國家/地區程式碼,貨幣程式碼

  • 時間和間隔數據:事件觸發器以精確的SI時間測量爲基礎

  • 天氣數據:基於天氣預報的保險費計算

  • 政治事件:預測市場決議

  • 體育賽事:預測市場決議和幻想體育合約

  • 地理位置數據:供應鏈跟蹤

  • 損害賠償:保險合約

  • 其他區塊鏈上發生的事件:互操作函數

  • 交易天然氣價格:gas價格oracles

  • 航班延誤:保險合約

在本節中,我們將在Solidity中檢查oracles的主要功能,oracle訂閱模式,計算oracles,去中心化的oracles和oracle客戶端實現。

主要功能

實際上,oracle可以實現爲鏈上智能合約系統和用於監控請求,查詢和返回數據的離線基礎設施。來自去中心化應用的數據請求通常是涉及許多步驟的異步過程。

首先,外部擁有帳戶將與去中心化應用進行交易,從而與oracle智能合約中定義的函數進行交互。此函數初始化對oracle的請求,除了可能包含回調函數和調度參數的補充資訊之外,還使用相關參數詳細說明所請求的數據。一旦驗證了此事務,就可以將oracle請求視爲oracle合約發出的EVM事件,或者狀態更改; 參數可以被取出並用於從脫鏈數據源執行實際查詢。oracle可能需要處理請求的費用,回調的gas費用,以及訪問所請求數據的權限/權限。最後,結果數據由oracle所有者簽署,基本上證明在給定時間數據的價值,並在交易中交付給作出請求的去中心化應用 - 直接或通過oracle合約。根據調度參數,oracle可以定期廣播進一步更新數據的事務,例如日終定價資訊。

可能有一系列替代方案。可以從外部擁有帳戶請求數據並直接返回數據,從而無需使用oracle智能合約。類似地,可以向物聯網啓用的硬體傳感器發出請求和響應。因此,oracles可以是人類,軟體或基於硬體的。

oracle的主要功能可概括如下:

  • 迴應去中心化應用的查詢

  • 解析查詢

  • 檢查是否符合付款和數據權限/權利義務

  • 從脫鏈源查詢數據

  • 在交易中籤署數據

  • 向網路廣播交易

  • 進一步安排交易

訂閱模式

上述訂閱模式是典型的請求——響應模式,常見於客戶端——伺服器體系結構中。雖然這是一種有用的消息傳遞模式,允許應用程式進行雙向對話,但它是一種相對簡單的模式,在某些條件下可能不合適。例如,需要oracle提供利率的智能債券可能需要在請求-響應模式下每天請求數據,以確保利率始終是正確的。鑑於利率不經常變化,發佈——訂閱模式在這裏可能更合適,尤其是考慮到以太坊的有限帶寬。

發佈——訂閱是一種模式,其中發佈者,這裏是oracles,不直接向接收者發送消息,而是將發佈的消息分到不同的類中。訂閱者能夠表達對一個或多個類的興趣並僅查詢那些感興趣的消息。在這種模式下,oracle可以將利率寫入其自己的內部儲存,當且僅當它發生變化時。多個訂閱的去中心化應用可以簡單地從oracle合約中讀取它,從而減少對網路帶寬的影響,同時最大限度地降低儲存成本。

在廣播或多播模式中,oracle會將所有消息發佈到一個頻道,訂閱合約將在各種訂閱模式下收聽該頻道。例如,oracle可能會將消息發佈到密碼貨幣匯率通道。訂閱智能合約如果需要時間序列,例如移動平均計算,則可以請求信道的全部內容; 另一個可能只需要最後一個價格來計算現貨價格。在oracle不需要知道訂閱合約的身份的情況下,廣播模式是合適的。

數據認證

如果我們假設去中心化應用查詢的數據源既具有權威性又值得信賴,那麼一個懸而未決的問題仍然存在:假設oracle和查詢/響應機制可能由不同的實體操作,我們如何才能信任這種機制?數據在傳輸過程中可能會被篡改,因此脫鏈方法能夠證明返回數據的完整性至關重要。數據認證的兩種常用方法是真實性證明和可信執行環境(TEE)。

真實性證明是加密保證,證明數據未被篡改。基於各種證明技術(例如,數位簽章證明),它們有效地將信任從數據載體轉移到證明者,即證明方法的提供者。通過在鏈上驗證真實性,智能合約能夠在對其進行操作之前驗證數據的完整性。Oraclize[1]是利用各種真實性證明的oracle服務的一個例子。目前可用於以太坊主網路的數據查詢是TLSNotary Proof [2]。TLSNotary Proofs允許客戶端向第三方提供客戶端和伺服器之間發生HTTPS Web流量的證據。雖然HTTPS本身是安全的,但它不支持數據簽名。結果是,TLSNotary證明依賴於TLSNotary(通過PageSigner [3])簽名。TLSNotary Proofs利用傳輸層安全性(TLS)協議,使得在訪問數據後對數據進行簽名的TLS主密鑰在三方之間分配:伺服器(oracle),受審覈方(Oraclize)和核數師。Oraclize使用Amazon Web Services(AWS)虛擬機實例作爲審覈員,可以證明自它實例化以來未經修改[4]。此AWS實例儲存TLSNotary機密,允許其提供誠實證明。雖然它提供了比純信任查詢/響應機制更高的數據篡改保證,但這種方法確實需要假設亞馬遜本身不會篡改VM實例。

TownCrier [5,6]是基於可信執行環境的經過身份驗證的數據饋送oracle系統; 這些方法採用不同的機制,利用基於硬體的安全區域來驗證數據的完整性。TownCrier使用英特爾的SGX(Software Guard eXtensions)來確保HTTPS查詢的響應可以被驗證爲可靠。SGX提供完整性保證,確保在安全區內運行的應用程式受到CPU的保護,防止任何其他進程被篡改。它還提供機密性,確保在安全區內運行時應用程式的狀態對其他進程不透明。最後,SGX允許證明,通過生成數位簽章的證據,證明應用程式 - 通過其構建的雜湊安全地識別 - 實際上是在安全區內運行。通過驗證此數位簽章,去中心化式應用程式可以證明TownCrier實例在SGX安全區內安全運行。反過來,這證明實例沒有被篡改,因此TownCrier發出的數據是真實的。機密性屬性還允許TownCrier通過允許使用TownCrier實例的公鑰加密數據查詢來處理私有數據。通過在諸如SGX的安全區內運行oracle的查詢/響應機制,可以有效地將其視爲在受信任的第三方硬體上安全運行,確保所請求的數據被返回到未被禁用的狀態(假設我們信任Intel/SGX)。

計算 oracles

到目前爲止,我們只是在請求和提供數據的背景下討論了oracles。然而,oracles也可用於執行任意計算,這一功能在以太坊固有的區塊gas限制和相對昂貴的計算成本的情況下特別有用; Vitalik本人指出,與現有的集中服務相比,以太坊的計算成本效率低了一百萬倍[7]。計算oracles可以用於對一組輸入執行相關計算,而不是僅僅中繼查詢結果,並返回計算結果,這可能是在鏈上計算不可行的。例如,可以使用計算oracle執行計算密集型迴歸計算,以估計債券合約的收益率。

Oraclize提供的服務允許去中心化應用請求輸出在沙盒AWS虛擬機中執行的計算。AWS實例從包含在上傳到IPFS的存檔中的用戶配置的Dockerfile創建可執行容器。根據請求,Oraclize使用其雜湊查詢此存檔,然後在AWS上初始化並執行Docker容器,將作爲環境變數提供給應用程式的任何參數傳遞。容器化應用程式根據時間限制執行計算,並且必須將結果寫入標準輸出,Oraclize可以將其返回到去中心化應用。Oraclize目前在可審覈的t2.micro AWS實例上提供此服務。

作爲可驗證的oracle真理的標準,“cryptlet”的概念已被正式化爲Microsoft更廣泛的ESC框架[8]的一部分。Cryptlet在加密的封裝內執行,該封裝抽象出基礎設施,例如I/O,並附加了CryptoDelegate,以便自動對傳入和傳出的消息進行簽名,驗證和驗證。Cryptlet支持分佈式事務,因此合約邏輯可以以ACID方式處理複雜的多步驟,多區塊鏈和外部系統事務。這允許開發人員創建便攜,隔離和私有的真相解決方案,以便在智能合約中使用。Cryptlet遵循以下格式:

public class SampleContractCryptlet : Cryptlet
  {
        public SampleContractCryptlet(Guid id, Guid bindingId, string name, string address, IContainerServices hostContainer, bool contract)
            : base(id, bindingId, name, address, hostContainer, contract)
        {
            MessageApi =
                new CryptletMessageApi(GetType().FullName, new SampleContractConstructor())

TrueBit [9]是可擴展和可驗證的離線計算的解決方案。它引入了一個求解器和驗證器系統,分別執行計算和驗證。如果解決方案受到挑戰,則在鏈上執行對計算子集的迭代驗證過程 - 一種“驗證遊戲”。遊戲通過一系列迴圈進行,每個迴圈遞迴地檢查計算的越來越小的子集。遊戲最終進入最後一輪,挑戰是微不足道的,以至於評委 - 以太坊礦工 - 可以對挑戰是否合理,在鏈上進行最終裁決。實際上,TrueBit是一個計算市場的實現,允許去中心化應用支付可在網路外執行的可驗證計算,但依靠以太坊來強制執行驗證遊戲的規則。理論上,這使無信任的智能合約能夠安全地執行任何計算任務。

TrueBit等系統有廣泛的應用,從機器學習到任何工作量證明的驗證。後者的一個例子是Doge-Ethereum橋,它利用TrueBit來驗證Dogecoin的工作量證明,Scrypt,一種難以在以太坊塊gas限制內計算的記憶體密集和計算密集型函數。通過在TrueBit上執行此驗證,可以在以太坊的Rinkeby測試網路上的智能合約中安全地驗證Dogecoin交易。

去中心化的 oracles

上面概況的機制都描述了依賴於可信任權威的集中式oracle系統。雖然它們應該足以滿足許多應用,但它們確實代表了以太坊網路中的中心故障點。已經提出了許多圍繞去中心化oracle作爲確保數據可用性手段的方案,以及利用鏈上數據聚合系統創建獨立數據提供者網路。

ChainLink [10]提出了一個去中心化oracle網路,包括三個關鍵的智能合約:信譽合約,訂單匹配合約,彙總合約和數據提供商的脫鏈註冊。信譽合約用於跟蹤數據提供商的績效。聲譽合約中的分數用於填充離線註冊表。訂單匹配合約使用信譽合約從oracles中選擇出價。然後,它最終確定服務級別協議(SLA),其中包括查詢參數和所需的oracles數量。這意味着購買者無需直接與個別的oracles交易。聚合合約從多個oracles收集使用提交/顯示方案提交的響應,計算查詢的最終集合結果,

這種去中心化方法的主要挑戰之一是彙總函數的制定。ChainLink建議計算加權響應,允許爲每個oracle響應報告有效性分數。在這裏檢測“無效”分數是非常重要的,因爲它依賴於前提:由對等體提供的響應偏差測量的外圍數據點是不正確的。基於響應分佈中的oracle響應的位置來計算有效性分數可能會使正確答案超過普通答案。因此,ChainLink提供了一組標準的聚合合約,但也允許指定自定義的聚合合約。

一個相關的想法是SchellingCoin協議[11]。在這裏,多個參與者報告價值,並將中位數作爲“正確”答案。報告者必須提供重新分配的存款,以支持更接近中位數的價值,從而激勵報告與其他價值相似的價值。一個共同的價值,也稱爲Schelling Point,受訪者可能認爲這是一個自然而明顯的協調目標,預計將接近實際價值。

Teutsch最近提出了一種新的去中心化脫鏈數據可用性設計oracle [12]。該設計利用專用的工作證明區塊鏈,該區塊鏈能夠正確地報告在給定時期內的註冊數據是否可用。礦工嘗試下載,儲存和傳播所有當前註冊的數據,因此保證數據在本地可用。雖然這樣的系統在每個挖掘節點儲存和傳播所有註冊數據的意義上是昂貴的,但是系統允許通過在註冊週期結束之後釋放數據來重用儲存。

Solidity中的Oracle客戶端接口

下面是一個Solidity範例,演示如何使用API從Oraclize連續輪詢ETH/USD價格並以可用的方式儲存結果。:

/*
   ETH/USD price ticker leveraging CryptoCompare API

   This contract keeps in storage an updated ETH/USD price,
   which is updated every 10 minutes.
 */

pragma solidity ^0.4.1;
import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";

/*
   "oraclize_" prepended methods indicate inheritance from "usingOraclize"
 */
contract EthUsdPriceTicker is usingOraclize {

    uint public ethUsd;

    event newOraclizeQuery(string description);
    event newCallbackResult(string result);

    function EthUsdPriceTicker() payable {
        // signals TLSN proof generation and storage on IPFS
        oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS);

        // requests query
        queryTicker();
    }

    function __callback(bytes32 _queryId, string _result, bytes _proof) public {
        if (msg.sender != oraclize_cbAddress()) throw;
        newCallbackResult(_result);

        /*
         * parse the result string into an unsigned integer for on-chain use
         * uses inherited "parseInt" helper from "usingOraclize", allowing for
         * a string result such as "123.45" to be converted to uint 12345
         */
        ethUsd = parseInt(_result, 2);

        // called from callback since we're polling the price
        queryTicker();
    }

    function queryTicker() public payable {
        if (oraclize_getPrice("URL") > this.balance) {
            newOraclizeQuery("Oraclize query was NOT sent, please add some ETH to cover for the query fee");
        } else {
            newOraclizeQuery("Oraclize query was sent, standing by for the answer..");

            // query params are (delay in seconds, datasource type, datasource argument)
            // specifies JSONPath, to fetch specific portion of JSON API result
            oraclize_query(60 * 10, "URL", "json(https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,GBP).USD");
        }
    }
}

要與Oraclize集成,合約EthUsdPriceTicker必須是usingOraclize的子項;usingOraclize合約在oraclizeAPI檔案中定義。數據請求是使用oraclize_query()函數生成的,該函數繼承自usingOraclize合約。這是一個重載函數,至少需要兩個參數:

  • 支持的數據源,例如URL,WolframAlpha,IPFS或計算

  • 給定數據源的參數,可能包括使用JSON或XML解析助手

價格查詢在queryTicke()函數中執行。爲了執行查詢,Oraclize要求在以太網中支付少量費用,包括將結果傳輸和處理到callback()函數的gas成本以及隨附的服務附加費。此數量取決於數據源,如果指定,則取決於所需的真實性證明類型。一旦查詢到數據,callback()函數由Oraclize控制的帳戶調用,該帳戶被允許進行回調; 它傳遞響應值和唯一的queryId參數,作爲範例,它可用於處理和跟蹤來自Oraclize的多個掛起的回調。

金融數據提供商Thomson Reuters還爲以太坊提供了一項名爲BlockOne IQ的oracle服務,允許在私有或許可網路上運行的智能合約請求市場和參考數據[13]。下面是oracle的接口,以及將發出請求的客戶端合約:

pragma solidity ^0.4.11;

contract Oracle {
    uint256 public divisor;
    function initRequest(uint256 queryType, function(uint256) external onSuccess, function(uint256) external onFailure) public returns (uint256 id);
    function addArgumentToRequestUint(uint256 id, bytes32 name, uint256 arg) public;
    function addArgumentToRequestString(uint256 id, bytes32 name, bytes32 arg) public;
    function executeRequest(uint256 id) public;
    function getResponseUint(uint256 id, bytes32 name) public constant returns(uint256);
    function getResponseString(uint256 id, bytes32 name) public constant returns(bytes32);
    function getResponseError(uint256 id) public constant returns(bytes32);
    function deleteResponse(uint256 id) public constant;
}

contract OracleB1IQClient {

    Oracle private oracle;
    event LogError(bytes32 description);

    function OracleB1IQClient(address addr) public payable {
        oracle = Oracle(addr);
        getIntraday("IBM", now);
    }

    function getIntraday(bytes32 ric, uint256 timestamp) public {
        uint256 id = oracle.initRequest(0, this.handleSuccess, this.handleFailure);
        oracle.addArgumentToRequestString(id, "symbol", ric);
        oracle.addArgumentToRequestUint(id, "timestamp", timestamp);
        oracle.executeRequest(id);
    }

    function handleSuccess(uint256 id) public {
        assert(msg.sender == address(oracle));
        bytes32 ric = oracle.getResponseString(id, "symbol");
        uint256 open = oracle.getResponseUint(id, "open");
        uint256 high = oracle.getResponseUint(id, "high");
        uint256 low = oracle.getResponseUint(id, "low");
        uint256 close = oracle.getResponseUint(id, "close");
        uint256 bid = oracle.getResponseUint(id, "bid");
        uint256 ask = oracle.getResponseUint(id, "ask");
        uint256 timestamp = oracle.getResponseUint(id, "timestamp");
        oracle.deleteResponse(id);
        // Do something with the price data..
    }

    function handleFailure(uint256 id) public {
        assert(msg.sender == address(oracle));
        bytes32 error = oracle.getResponseError(id);
        oracle.deleteResponse(id);
        emit LogError(error);
    }

}

使用initRequest()函數啓動數據請求,該函數除了兩個回調函數之外,還允許指定查詢類型(在此範例中,是對日內價格的請求)。 這將返回一個uint256識別碼,然後可以使用該識別碼提供其他參數。addArgumentToRequestString()函數用於指定RIC(Reuters Instrument Code),此處用於IBM股票,addArgumentToRequestUint()允許指定時間戳。現在,傳入block.timestamp的別名將查詢IBM的當前價格。然後由executeRequest()函數執行該請求。處理完請求後,oracle合約將使用查詢識別碼調用onSuccess回調函數,允許查詢結果數據,否則在查詢失敗時使用錯誤程式碼進行onFailure回調。成功查詢的可用欄位包括開盤價,最高價,最低價,收盤價(OHLC)和買/賣價。

Reality Keys [14]允許使用POST請求對事實進行離線請求。響應以加密方式簽名,允許在鏈上進行驗證。在這裏,請求使用blockr.io API在特定時間檢查比特幣區塊鏈上的帳戶餘額:

wget -qO- https://www.realitykeys.com/api/v1/blockchain/new --post-data="chain=XBT&address=1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX&which_total=total_received&comparison=ge&value=1000&settlement_date=2015-09-23&objection_period_secs=604800&accept_terms_of_service=current&use_existing=1"

對於此範例,參數允許指定區塊鏈,要查詢的金額(總收到金額或最終餘額)以及要與提供的值進行比較的結果,從而允許真或假的響應。除了“signature_v2”欄位之外,生成的JSON物件還包括返回值,該欄位允許使用ecrecover()函數在智能合約中驗證結果:

"machine_resolution_value" : "29665.80352",
"signature_v2" : {
    "fact_hash" : "aadb3fa8e896e56bb13958947280047c0b4c3aa4ab8c07d41a744a79abf2926b",
    "ethereum_address" : "6fde387af081c37d9ffa762b49d340e6ae213395",
    "base_unit" : 1,
    "signed_value" : "0000000000000000000000000000000000000000000000000000000000000001",
    "sig_r" : "a2cd9dc040e393299b86b1c21cbb55141ef5ee868072427fc12e7cfaf8fd02d1",
    "sig_s" : "8f3199b9c5696df34c5193afd0d690241291d251a5d7b5c660fa8fb310e76f80",
    "sig_v" : 27
}

爲了驗證簽名,ecrecover()可以確定數據確實由ethereum_address簽名,如下所示。fact_hash和signed_value經過雜湊處理,並將三個簽名參數傳遞給ecrecover():

bytes32 result_hash = sha3(fact_hash, signed_value);
address signer_address = ecrecover(result_hash, sig_v, sig_r, sig_s);
assert(signer_address == ethereum_address);
uint256 result = uint256(signed_value) / base_unit;
// Do something with the result..

results matching ""

    No results matching ""