14 Tests working plus enhanced contract and fixed library.

This commit is contained in:
Hernán Di Pietro 2021-10-06 16:39:50 -03:00
parent 25716e8f2a
commit f83e592556
3 changed files with 147 additions and 79 deletions

View File

@ -8,7 +8,7 @@ const approvalProgramFilename = 'teal/pricekeeper.teal'
const clearProgramFilename = 'teal/clearstate.teal'
class PricecasterLib {
constructor(algodClient, ownerAddr = undefined) {
constructor (algodClient, ownerAddr = undefined) {
this.algodClient = algodClient
this.ownerAddr = ownerAddr
this.minFee = 1000
@ -139,7 +139,7 @@ class PricecasterLib {
this.createApp = async function (sender, validatorAddr, symbol, signCallback) {
const localInts = 0
const localBytes = 0
const globalInts = 6
const globalInts = 5
const globalBytes = 2
// declare onComplete as NoOp
@ -221,7 +221,7 @@ class PricecasterLib {
params.fee = this.minFee
params.flatFee = true
console.log(appArgs)
// console.log(appArgs)
const txns = []
const enc = new TextEncoder()
@ -355,27 +355,30 @@ class PricecasterLib {
* @param {BigInt} exp Exponent (positive)
* @param {BigInt} slot Valid-slot of price aggregation
* @param {Uint8Array} sk Signing key.
* @param {string} header (optional) Message header. 'PRICEDATA' if undefined.
* @param {BigInt} appId (optional) AppId. Default is this contract appId.
* @param {number} version (optional) Version. Default is 1 if undefined.
* @param {BigInt} ts (optional) Timestamp of message. Current system ts if undefined.
* @returns A base64-encoded message.
*/
this.createMessage = function (nonce, symbol, price, exp, confidence, slot, sk) {
const buf = Buffer.alloc(146)
buf.write('PRICEDATA', 0)
buf.writeInt8(1, 9)
buf.writeBigUInt64BE(BigInt(this.appId), 10)
buf.writeBigUInt64BE(nonce, 18)
buf.write(symbol, 26)
buf.writeBigUInt64BE(price, 42)
buf.writeBigUInt64BE(exp, 50)
buf.writeBigUInt64BE(confidence, 58)
buf.writeBigUInt64BE(slot, 66)
buf.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 1000)), 74)
this.createMessage = function (symbol, price, exp, confidence, slot, sk, header, appId, version, ts) {
const buf = Buffer.alloc(138)
buf.write(header === undefined ? 'PRICEDATA' : header, 0)
buf.writeInt8(version === undefined ? 1 : version, 9)
buf.writeBigUInt64BE(appId === undefined ? BigInt(this.appId) : appId, 10)
buf.write(symbol, 18)
buf.writeBigUInt64BE(price, 34)
buf.writeBigUInt64BE(exp, 42)
buf.writeBigUInt64BE(confidence, 50)
buf.writeBigUInt64BE(slot, 58)
buf.writeBigUInt64BE(ts === undefined ? BigInt(Math.floor(Date.now() / 1000)) : ts, 66)
const digestu8 = Buffer.from(sha512_256(buf.slice(0, 82)), 'hex')
const digestu8 = Buffer.from(sha512_256(buf.slice(0, 74)), 'hex')
// console.log(digestu8.toString('base64'))
// console.log(this.approvalProgramHash)
const signature = Buffer.from(algosdk.tealSign(sk, digestu8, this.approvalProgramHash))
// console.log(Buffer.from(signature).toString('base64'))
signature.copy(buf, 82)
signature.copy(buf, 74)
// console.log(buf.toString('base64'))
return buf
}

View File

@ -6,12 +6,11 @@
// App-globals:
// sym : byte[] Symbol to keep price for
// vaddr : byte[] Validator account
// nonce : uint64 last sequence ID
// price : uint64 current price
// stdev : uint64 current confidence (standard deviation)
// slot : uint64 slot of this onchain publication
// exp : uint64 exponent (fixed point position)
// ts : uint64 timestamp
// ts : uint64 last timestamp
//
// Slots:
// 0 Input message block
@ -23,7 +22,6 @@
// 9 header Literal "PRICEDATA"
// 1 version int8 (Must be 1)
// 8 dest This appId
// 8 nonce uint64 Sequence identifier
// 16 symbol String filled with spaces e.g ("ALGO/USD ")
// 8 price Price. 64bit integer.
// 8 priceexp Price exponent (fixed point position).
@ -33,7 +31,7 @@
// 32 s Signature s-component
// 32 r Signature r-component
//
// Size: 146 bytes.
// Size: 138 bytes.
//
// ------------------------------------------------------------------------------------------------
@ -79,8 +77,8 @@ int 16
assert
app_global_put
byte "nonce"
int 0
byte "ts"
global LatestTimestamp
app_global_put
b success
@ -97,7 +95,7 @@ int 4
==
assert
// if this is one of dummy transactions(0 or 1), exit with success
// if this is one of dummy transactions(0, 1 or 2), exit with success
txn GroupIndex
int 3
!=
@ -122,7 +120,7 @@ store 0
load 0
len
int 146
int 138
==
assert
@ -151,19 +149,9 @@ txn ApplicationID
==
assert
// Check nonce
load 0
extract 18 8
btoi
byte "nonce"
app_global_get
>
assert
// Reject zero price
load 0
extract 42 8
extract 34 8
btoi
int 0
!=
@ -171,7 +159,7 @@ assert
// Reject zero slot
load 0
extract 66 8
extract 58 8
btoi
int 0
!=
@ -179,41 +167,42 @@ assert
// Reject out-of-range exponent
load 0
extract 50 8
extract 42 8
btoi
int 18
<=
assert
// Check timestamp order, must be +/-10 secs from last block ts
// Check timestamp:
// * must be higher than ts recorded in global state
// * must be lower than current block stamp + 10s
// (TODO: check this again)
load 0
extract 74 8
extract 66 8
btoi
dup
dup
global LatestTimestamp
int 10
-
<
//assert
global LatestTimestamp
int 10
+
swap
<
assert
byte "ts"
app_global_get
>
//assert
assert
// ed25519verify args in stack:
// data (hash of message)
load 0
extract 0 82
extract 0 74
sha512_256
// (B) signature
load 0
extract 82 64
extract 74 64
// validator-address
byte "vaddr"
@ -223,45 +212,39 @@ app_global_get
ed25519verify
int 1
==
//assert
assert
// ----------------------------------------------------------------------------
// Verified. Store data to app globals.
// ----------------------------------------------------------------------------
byte "nonce"
load 0
extract 18 8
btoi
app_global_put
byte "ts"
load 0
extract 74 8
extract 66 8
btoi
app_global_put
byte "price"
load 0
extract 42 8
extract 34 8
btoi
app_global_put
byte "exp"
load 0
extract 50 8
extract 42 8
btoi
app_global_put
byte "stdev"
byte "conf"
load 0
extract 58 8
extract 50 8
btoi
app_global_put
byte "slot"
load 0
extract 66 8
extract 58 8
btoi
app_global_put

View File

@ -6,34 +6,45 @@ const chai = require('chai')
chai.use(require('chai-as-promised'))
// Test general configuration for Betanet
const validatorAddr = 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU'
const validatorMnemo = 'assault approve result rare float sugar power float soul kind galaxy edit unusual pretty tone tilt net range pelican avoid unhappy amused recycle abstract master'
const otherAddr = 'DMTBK62XZ6KNI7L5E6TRBTPB4B3YNVB4WYGSWR42SEV4XKV4LYHGBW4O34'
const otherMnemo = 'old agree harbor cost pink fog chunk hope vital used rural soccer model acquire clown host friend bring marriage surge dirt surge slab absent punch'
const symbol = 'BTC/USD '
const signatures = {}
signatures[validatorAddr] = algosdk.mnemonicToSecretKey(validatorMnemo)
signatures[otherAddr] = algosdk.mnemonicToSecretKey(otherMnemo)
const VALIDATOR_ADDR = 'OPDM7ACAW64Q4VBWAL77Z5SHSJVZZ44V3BAN7W44U43SUXEOUENZMZYOQU'
const VALIDATOR_MNEMO = 'assault approve result rare float sugar power float soul kind galaxy edit unusual pretty tone tilt net range pelican avoid unhappy amused recycle abstract master'
const OTHER_ADDR = 'DMTBK62XZ6KNI7L5E6TRBTPB4B3YNVB4WYGSWR42SEV4XKV4LYHGBW4O34'
const OTHER_MNEMO = 'old agree harbor cost pink fog chunk hope vital used rural soccer model acquire clown host friend bring marriage surge dirt surge slab absent punch'
const SIGNATURES = {}
SIGNATURES[VALIDATOR_ADDR] = algosdk.mnemonicToSecretKey(VALIDATOR_MNEMO)
SIGNATURES[OTHER_ADDR] = algosdk.mnemonicToSecretKey(OTHER_MNEMO)
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
const VALID_SYMBOL = 'BTC/USD '
const VALID_PRICE = BigInt(485265555)
const VALID_EXPONENT = BigInt(4)
const VALID_CONF = BigInt(1400000)
const VALID_SLOT = BigInt(104566700)
function signCallback (sender, tx) {
const txSigned = tx.signTxn(signatures[sender].sk)
const txSigned = tx.signTxn(SIGNATURES[sender].sk)
return txSigned
}
describe('Price-Keeper contract tests', function () {
let pclib
let algodClient
let appId
let lastTs
before(async function () {
algodClient = new algosdk.Algodv2('', 'https://api.betanet.algoexplorer.io', '')
pclib = new PricecasterLib.PricecasterLib(algodClient)
console.log('Clearing accounts of all previous apps...')
const appsTo = await tools.readCreatedApps(algodClient, validatorAddr)
const appsTo = await tools.readCreatedApps(algodClient, VALIDATOR_ADDR)
for (let i = 0; i < appsTo.length; i++) {
console.log('Clearing ' + appsTo[i].id)
try {
const txId = await pclib.deleteApp(validatorAddr, signCallback, appsTo[i].id)
const txId = await pclib.deleteApp(VALIDATOR_ADDR, signCallback, appsTo[i].id)
await pclib.waitForConfirmation(txId)
} catch (e) {
console.error('Could not delete application! Reason: ' + e)
@ -41,18 +52,89 @@ describe('Price-Keeper contract tests', function () {
}
console.log('Creating new app...')
const txId = await pclib.createApp(validatorAddr, validatorAddr, symbol, signCallback)
const txId = await pclib.createApp(VALIDATOR_ADDR, VALIDATOR_ADDR, VALID_SYMBOL, signCallback)
const txResponse = await pclib.waitForTransactionResponse(txId)
const appId = pclib.appIdFromCreateAppResponse(txResponse)
appId = pclib.appIdFromCreateAppResponse(txResponse)
pclib.setAppId(appId)
console.log('App Id: %d', appId)
})
it('Must create app with proper initial global state', async function () {
const stVAddr = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'vaddr')
const stSym = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'sym')
expect(Buffer.from(stSym, 'base64').toString()).to.equal(VALID_SYMBOL)
expect(stVAddr.toString()).to.equal(VALIDATOR_ADDR)
})
it('Must fail to create app with bad symbol', async function () {
await expect(pclib.createApp(validatorAddr, validatorAddr, 'XXXXX', signCallback)).to.be.rejectedWith('Bad Request')
await expect(pclib.createApp(VALIDATOR_ADDR, VALIDATOR_ADDR, 'XXXXX', signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must accept valid message and store data', async function () {
const msgb64 = pclib.createMessage(BigInt(1), 'BTC/USD ', 48526.50, 8.99999967, signatures[validatorAddr].sk)
console.log(msgb64)
await expect(pclib.submitMessage(validatorAddr, msgb64, signCallback)).to.eventually.have.length(52)
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk)
const txid = await pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)
expect(txid).to.have.length(52)
await pclib.waitForTransactionResponse(txid)
//console.log(await tools.printAppGlobalState(algodClient, appId, VALIDATOR_ADDR))
const stPrice = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'price')
const stExp = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'exp')
const stConf = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'conf')
const stSlot = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'slot')
expect(stPrice.toString()).to.equal(VALID_PRICE.toString())
expect(stExp.toString()).to.equal(VALID_EXPONENT.toString())
expect(stSlot.toString()).to.equal(VALID_SLOT.toString())
expect(stConf.toString()).to.equal(VALID_CONF.toString())
})
it('Must accept second message with different price', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE + BigInt(400), VALID_EXPONENT + BigInt(3), VALID_CONF + BigInt(2), VALID_SLOT + BigInt(100), SIGNATURES[VALIDATOR_ADDR].sk)
const txid = await pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)
expect(txid).to.have.length(52)
await pclib.waitForTransactionResponse(txid)
const stPrice = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'price')
const stExp = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'exp')
const stConf = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'conf')
const stSlot = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'slot')
expect(stPrice.toString()).to.equal((VALID_PRICE + BigInt(400)).toString())
expect(stExp.toString()).to.equal((VALID_EXPONENT + BigInt(3)).toString())
expect(stSlot.toString()).to.equal((VALID_SLOT + BigInt(100)).toString())
expect(stConf.toString()).to.equal((VALID_CONF + BigInt(2)).toString())
lastTs = await tools.readAppGlobalStateByKey(algodClient, appId, VALIDATOR_ADDR, 'ts')
})
it('Must reject non-validator as signer', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, VALID_SLOT, SIGNATURES[OTHER_ADDR].sk)
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject non-validator as sender', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk)
await expect(pclib.submitMessage(OTHER_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject future timestamp', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk, undefined, undefined, undefined, BigInt(lastTs + 200))
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject old timestamp', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF,VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk, undefined, undefined, undefined, BigInt(lastTs - 999999))
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject zero-priced message', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, BigInt(0), VALID_EXPONENT, VALID_CONF, VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk)
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject out-of-range exponent', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, BigInt(1000000), VALID_CONF, VALID_SLOT, SIGNATURES[VALIDATOR_ADDR].sk)
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject zero slot', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, BigInt(0), SIGNATURES[VALIDATOR_ADDR].sk)
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject bad header', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, BigInt(0), SIGNATURES[VALIDATOR_ADDR].sk, 'BADHEADER')
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject bad destination appId', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, BigInt(0), SIGNATURES[VALIDATOR_ADDR].sk, undefined, BigInt(100))
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
it('Must reject bad message version', async function () {
const msgBuffer = pclib.createMessage(VALID_SYMBOL, VALID_PRICE, VALID_EXPONENT, VALID_CONF, BigInt(0), SIGNATURES[VALIDATOR_ADDR].sk, undefined, undefined, 0)
await expect(pclib.submitMessage(VALIDATOR_ADDR, msgBuffer, signCallback)).to.be.rejectedWith('Bad Request')
})
})