📦 Chapter II - Trust no one
Now that we've posted the price package within the transaction, we need to somehow verify and unpack this data in the contract.
RedStone Oracles extension​
In order to do this, the contract code has to have
access to the redstone-protocol
library from the RedStone Oracles ecosystem.
First we need to install the library:
npm install --save redstone-protocol
We will now introduce a Warp Plugin 🔌 - SmartWeave
extension that will make redstone-protocol
available for the contract code.
Each Warp Plugin must implement two functions:
process
- it performs the plugin's logic. In case of SmartWeave extensions, theinput
parameter is simply theSmartWeave.extensions
object - where you can attach your libraries.type
- the name of the plugin - required by the SDK to differentiate between differentiate types of plugins. In case of SmartWeave extension - the type must begin withsmartweave-extension-
prefix.
The full API of a Warp Plugin interface is here.
import * as redstone from "redstone-protocol";
export class RedStonePlugin {
process(input) {
input.redstone = redstone;
}
type() {
return 'smartweave-extension-redstone';
}
}
and plug it into our Warp
instance:
const warp = WarpFactory.forMainnet()
.use(new RedStonePlugin());
Always double-check whether the code of the library that you add via an extension is deterministic!
Also - consider using SmartWeave extensions as a last resort and use it only if attaching the library code into the contract code itself is not possible. Attaching libraries code into the contract increases decentralization and makes it easier to evaluate the contract - as the clients don't need to attach specific plugins. A good example of a contract with attached libraries is WeaveDB's solution.
Now that we have the Warp
instance configured with a new extension, we can use this extension in our contract code to verify
the incoming price packages.
Defining authorised signers​
Each price package is signed by one of the nodes from the given RedStone Oracles data service. The list of the nodes and their EVM address can be obtained from here.
Let's use the constructor to define a list of authorised signers. The Warp SDK ensures that the constructor will be called exactly once, as a first function during contract evaluation.
// constructor - https://docs.warp.cc/docs/sdk/advanced/constructor
if (input.function === '__init') {
// redstone-avalanche-prod nodes
state.redstoneAuthorizedSigners = [
'0x1eA62d73EdF8AC05DfceA1A34b9796E937a29EfF',
'0x2c59617248994D12816EE1Fa77CE0a64eEB456BF',
'0x12470f7aBA85c8b81D63137DD5925D6EE114952b',
'0x109B4a318A4F5ddcbCA6349B45f881B4137deaFB',
'0x83cbA8c619fb629b81A65C2e67fE15cf3E3C9747'
];
return {state};
}
To use a constructor, we must deploy a contract with a proper manifest (with useConstructor
option set to true
) - that will specify that the SDK must
call the constructor function before evaluating any interaction:
const {contractTxId} = await warp.deploy({
wallet: signer,
initState: JSON.stringify({}),
src: CONTRACT_CODE,
evaluationManifest: {
evaluationOptions: {
useConstructor: true
}
}
});
Verifying data package​
Having authorised signers defined and our new RedStone Oracles Protocol extension added, we can write a function that will extract price, its timestamp and verify the signature.
function extractValueFromPricePackage(pricePackage, authorizedSigners) {
// redstone-protocol extension
const redstone = SmartWeave.extensions.redstone;
// parse stringified version
const pricePackageObj = JSON.parse(pricePackage);
// create a SignedDataPackage from object
const signedDataPackage = redstone.SignedDataPackage.fromObj(pricePackageObj);
// recover signer address from the package
const recoveredSignerAddress = signedDataPackage.recoverSignerAddress();
// check if signer address is within authorized signers
if (!authorizedSigners.includes(recoveredSignerAddress)) {
throw new ContractError(`Unauthorized price package signer: ${recoveredSignerAddress}`);
}
return {v: pricePackageObj.dataPoints[0].value, t: pricePackageObj.timestampMilliseconds};
}
In a real-life contract, the validation should be twofold:
- the signatures validation - already implemented
- the timestamps validation - if you want to teach your network with 'current' price data, you should verify whether
the incoming prices packages are not too 'old' (e.g. older than 1 Arweave block - roughly 2 minutes) and no older
than the previously accepted data (or - the other way - are not sent from the 'future' 🙂).
Consider this as a homework!
With this function ready, we can finally start writing our main contract function - train
if (input.function === 'train') {
// get the stringified price package from the interaction input
const pricePackage = input.pricePackage;
// verify the price package signature and extract price with a timestamp
const priceWithTimestamp = extractValueFromPricePackage(pricePackage, state.redstoneAuthorizedSigners);
logger.info('train:', priceWithTimestamp);
// add to data to train
state.toTrain.push(priceWithTimestamp);
// train the network every 5th package
if (state.toTrain.length == 5) {
logger.info('training');
doTrain(state);
}
return {state};
}
Did you notice the doTrain
function? It's the core of our smart contract - let's move to the next chapter!