mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
Go's decoder walks an object's members in document order and decodes each one that resolves to the field, so exactness decides WHICH FIELD a name reaches and POSITION decides which value survives. A reader that preferred the exact member wherever it sat keeps 1 where the chain keeps 9. The pair asks it both ways round, because neither half alone distinguishes the two rules. All three readers hold the chain's rule; nothing anywhere had said which one it was. 217 O vectors, 942 in all. Co-authored-by: Hanzo Dev <[email protected]>
1163 lines
43 KiB
Go
1163 lines
43 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
|
|
// The O-chain half of the corpus, and the Go reference's answers for it.
|
|
//
|
|
// WHERE THE CHAIN IS. `chains/oraclevm` is 213 lines and none of them are the
|
|
// chain: it is a re-export, and the O-Chain is `github.com/luxfi/oracle/vm` —
|
|
// 1628 lines. Reading the shim's size as the chain's size understates it by an
|
|
// order of magnitude, so this file imports the shim (which is what luxd loads,
|
|
// through type aliases) and reaches the one exported function the shim does
|
|
// not re-export, ComputeRequestID, from the canonical module.
|
|
//
|
|
// WHAT THIS CHAIN PUTS ON THE WIRE IS JSON. Not a codec's frame: `ParseBlock`
|
|
// is `json.Unmarshal` and `Bytes` is `json.Marshal`, so the O-chain's wire is
|
|
// whatever `encoding/json` writes for its structs — CB58 for an id, base64 for
|
|
// a byte slice, a list of numbers for a fixed array, RFC 3339 for a time, and
|
|
// field order taken from the struct declaration. Every one of those is a
|
|
// consensus surface here, and none of them is written down anywhere but in the
|
|
// Go standard library's behaviour.
|
|
//
|
|
// AND THE BLOCK ID IS A HASH OF THAT JSON, RE-MARSHALLED. `computeID` marshals
|
|
// the parsed block again and takes sha256 of the result — NOT of the bytes it
|
|
// was handed. Two consequences the vectors below pin:
|
|
//
|
|
// - a block does not round-trip its own id. `BuildBlock` hashes the block
|
|
// while `ID_` is still empty and then writes that id INTO the struct, so
|
|
// `Bytes()` is not the preimage of `ID()` and re-parsing a block the chain
|
|
// wrote gives it a different id. O_BLOCK_ID_SET is that block.
|
|
// - a member `encoding/json` accepts but omitempty later drops changes the
|
|
// id: `"observations":[]` parses to an empty slice, which re-marshals to
|
|
// nothing at all. O_BLOCK_EMPTY_ARRAY is the same block as O_BLOCK_EMPTY
|
|
// with two extra bytes on the wire and the SAME id.
|
|
//
|
|
// WHAT VERIFY DECIDES: nothing. `Block.Verify` is `return nil` with no
|
|
// condition in it, so on block vectors `syntactic` and `exec` are OK for
|
|
// everything that parses and MALFORMED for everything that does not. That is
|
|
// not a gap in the corpus, it is the chain: what O actually decides lives in
|
|
// the request/record/observation plane, and the other four ops below are
|
|
// where this chain's refusals are.
|
|
//
|
|
// THE GENESIS BLOCK'S ID IS NOT A FUNCTION OF THE GENESIS. `Initialize` builds
|
|
// it with `time.Unix(genesis.Timestamp, 0)`, which is LOCAL time, and
|
|
// `MarshalJSON` writes the offset. Measured on this box: the same genesis
|
|
// bytes give 6cf00752… under TZ unset (-08:00) and f4970a27… under
|
|
// Asia/Kolkata. Two validators in two timezones derive different genesis ids
|
|
// from one genesis file and are on different chains from block zero. So no
|
|
// vector here carries that id — a corpus answer that changes with the machine
|
|
// is not an answer — and O_GENESIS pins the genesis by the one thing that IS a
|
|
// function of its bytes: the canonical re-marshal of the parsed struct.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/chains/oraclevm"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
oracle "github.com/luxfi/oracle/vm"
|
|
"github.com/luxfi/runtime"
|
|
luxvm "github.com/luxfi/vm"
|
|
)
|
|
|
|
// The chain every O vector belongs to. Unlike Q, Z and F the O-chain hashes
|
|
// nothing but its own JSON into a block id, so this number does not enter any
|
|
// derivation — it is here so the identity vector can say which chain the
|
|
// evaluator was built for, and so a port configured for another one is named
|
|
// rather than agreeing by accident.
|
|
const oChain = 79 // 'O'
|
|
|
|
// The feeds the observation vectors are judged against, and the operators each
|
|
// admits. It is a corpus contract exactly as Z's genesis is: an implementation
|
|
// seeded with different feeds refuses observations this one accepts, and the
|
|
// disagreement would read as a rule when it is a configuration. O_GENESIS
|
|
// carries these bytes and asks every implementation what it parsed out of
|
|
// them, so "we applied the same configuration" is compared rather than assumed.
|
|
//
|
|
// Written by hand, not marshalled, because these bytes ARE the vector: a
|
|
// re-marshal would make the corpus's genesis a function of a struct that could
|
|
// change under it.
|
|
const oGenesisCfg = `{"version":1,"message":"conformance","timestamp":1000,` +
|
|
`"initialFeeds":[{"id":"2Kw2XL8QVSQHwJYKpWLktaxtwKuz7iYF5pqcauUHpmcrSjRPp",` +
|
|
`"name":"lux-usd","description":"","sources":null,"updateFreq":0,` +
|
|
`"policyHash":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],` +
|
|
`"operators":["NodeID-6Jswqk47s9PUcyCc88MMVwzgvHQxfLnA"],` +
|
|
`"createdAt":"1970-01-01T00:00:05Z","status":"active","metadata":null}]}`
|
|
|
|
// oOperator is the one operator lux-usd admits: the node id whose twenty bytes
|
|
// are all 0x01, which is what the CB58 above decodes to.
|
|
func oOperator() ids.NodeID { return nodeID(1) }
|
|
|
|
// oFeed is the feed lux-usd's id: the thirty-two bytes that are all 0x03,
|
|
// which is what the CB58 above decodes to.
|
|
func oFeed() ids.ID { return id(3) }
|
|
|
|
// oTime is a fixed instant in UTC. Every timestamp in the corpus is built here
|
|
// so that no vector's bytes depend on the machine's timezone — which, on this
|
|
// chain, is not a stylistic point (see the note on the genesis block above).
|
|
func oTime(sec int64, nsec int64) time.Time { return time.Unix(sec, nsec).UTC() }
|
|
|
|
// oFresh is a timestamp far enough ahead that `time.Since` is negative on it.
|
|
// SubmitObservation's staleness rule reads the clock, so an observation with a
|
|
// fixed past timestamp is stale forever and one with this timestamp is fresh
|
|
// for the next three hundred years. Both are wanted: the pair is what separates
|
|
// the staleness rule from the authorisation rule underneath it.
|
|
func oFresh() time.Time { return time.Date(2400, 1, 1, 0, 0, 0, 0, time.UTC) }
|
|
|
|
func oBytes(b byte, n int) []byte { return zBytes(b, n) }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Building the wire.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func oObservation(feed ids.ID, op ids.NodeID, ts time.Time, seed byte) *oraclevm.Observation {
|
|
obs := &oraclevm.Observation{
|
|
FeedID: feed,
|
|
Value: oBytes(seed, 8),
|
|
Timestamp: ts,
|
|
OperatorID: op,
|
|
Scheme: 1, // ML-DSA-65: the only scheme a strict-PQ O-chain admits
|
|
Signature: oBytes(seed+1, 16),
|
|
}
|
|
copy(obs.SourceMeta[:], oBytes(seed+2, 32))
|
|
return obs
|
|
}
|
|
|
|
func oAggregated(feed ids.ID, epoch uint64, seed byte) *oraclevm.AggregatedValue {
|
|
return &oraclevm.AggregatedValue{
|
|
FeedID: feed,
|
|
Epoch: epoch,
|
|
Value: oBytes(seed, 8),
|
|
Timestamp: oTime(2000, 0),
|
|
Observations: 3,
|
|
}
|
|
}
|
|
|
|
func oFeedRecord(feed ids.ID, name string) *oraclevm.Feed {
|
|
return &oraclevm.Feed{
|
|
ID: feed,
|
|
Name: name,
|
|
Operators: []ids.NodeID{oOperator()},
|
|
CreatedAt: oTime(5, 0),
|
|
Status: "active",
|
|
}
|
|
}
|
|
|
|
// oBlockWire writes a block the way the chain writes one. `ID_` is left empty
|
|
// because that is the state `BuildBlock` marshals in, and it is what makes the
|
|
// preimage the id is taken over.
|
|
func oBlockWire(parent ids.ID, height uint64, ts time.Time,
|
|
obs []*oraclevm.Observation, agg []*oraclevm.AggregatedValue,
|
|
feeds []*oraclevm.Feed) []byte {
|
|
b := &oraclevm.Block{
|
|
ID_: ids.Empty,
|
|
ParentID_: parent,
|
|
Height_: height,
|
|
Timestamp_: ts,
|
|
Observations: obs,
|
|
Aggregations: agg,
|
|
FeedUpdates: feeds,
|
|
}
|
|
w, err := json.Marshal(b)
|
|
must(err)
|
|
return w
|
|
}
|
|
|
|
func oParent() ids.ID { return id(7) }
|
|
|
|
// oNativeID is an id whose first thirty-one bytes are zero and whose last is
|
|
// the letter of a native chain. `ids.ID.MarshalJSON` writes those as the
|
|
// LETTER — "P" — and not as CB58, and `UnmarshalJSON` reads them back. An
|
|
// implementation that only knows CB58 writes a different block and derives a
|
|
// different id for it.
|
|
func oNativeID(letter byte) ids.ID {
|
|
var i ids.ID
|
|
i[31] = letter
|
|
return i
|
|
}
|
|
|
|
func oVectors() []Vector {
|
|
obs1 := oObservation(oFeed(), oOperator(), oTime(1500, 0), 0x20)
|
|
obs2 := oObservation(oFeed(), nodeID(2), oTime(1600, 0), 0x30)
|
|
agg := oAggregated(oFeed(), 1, 0x40)
|
|
feed := oFeedRecord(id(4), "eth-usd")
|
|
|
|
empty := oBlockWire(oParent(), 1, oTime(1000, 0), nil, nil, nil)
|
|
|
|
// The same block, written the way the chain writes one AFTER it has
|
|
// computed the id: `Bytes()` marshals with `ID_` filled in. Its id is
|
|
// therefore not the id it carries, which is the round-trip this chain does
|
|
// not have.
|
|
withID := func() []byte {
|
|
b := &oraclevm.Block{ParentID_: oParent(), Height_: 1, Timestamp_: oTime(1000, 0)}
|
|
b.ID_ = oBlockID(oBlockWire(oParent(), 1, oTime(1000, 0), nil, nil, nil))
|
|
w, err := json.Marshal(b)
|
|
must(err)
|
|
return w
|
|
}()
|
|
|
|
v := []Vector{
|
|
vec("O_GENESIS", "O", "genesis", []byte(oGenesisCfg)),
|
|
|
|
vec("O_BLOCK_EMPTY", "O", "block", empty),
|
|
vec("O_BLOCK_ZERO_HEIGHT", "O", "block",
|
|
oBlockWire(ids.Empty, 0, oTime(1000, 0), nil, nil, nil)),
|
|
vec("O_BLOCK_OBSERVATION", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs1}, nil, nil)),
|
|
vec("O_BLOCK_OBSERVATION_TWO", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs1, obs2}, nil, nil)),
|
|
vec("O_BLOCK_AGGREGATION", "O", "block",
|
|
oBlockWire(oParent(), 2, oTime(1000, 0), nil,
|
|
[]*oraclevm.AggregatedValue{agg}, nil)),
|
|
vec("O_BLOCK_FEED_UPDATE", "O", "block",
|
|
oBlockWire(oParent(), 3, oTime(1000, 0), nil, nil,
|
|
[]*oraclevm.Feed{feed})),
|
|
vec("O_BLOCK_MIXED", "O", "block",
|
|
oBlockWire(oParent(), 4, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs1},
|
|
[]*oraclevm.AggregatedValue{agg},
|
|
[]*oraclevm.Feed{feed})),
|
|
|
|
// The encoder's own edges, each a place two implementations can write
|
|
// one value differently and derive two ids for one block.
|
|
//
|
|
// omitempty on a []byte: present when it carries bytes, absent when it
|
|
// is empty, and an implementation that wrote `"aggProof":""` for the
|
|
// empty one names a different block. The pair is the check.
|
|
vec("O_BLOCK_AGG_PROOFS", "O", "block",
|
|
oBlockWire(oParent(), 2, oTime(1000, 0), nil,
|
|
[]*oraclevm.AggregatedValue{oProved(0x41, 8, 4)}, nil)),
|
|
vec("O_BLOCK_AGG_EMPTY_PROOF", "O", "block",
|
|
oBlockWire(oParent(), 2, oTime(1000, 0), nil,
|
|
[]*oraclevm.AggregatedValue{oProved(0x41, 0, 0)}, nil)),
|
|
|
|
// A feed carrying every member that has a rendering of its own: a
|
|
// non-nil list of strings, a duration in nanoseconds, a map — whose
|
|
// keys come out SORTED and are given here out of order — and a
|
|
// description holding the three runes Marshal escapes.
|
|
vec("O_BLOCK_FEED_RICH", "O", "block",
|
|
oBlockWire(oParent(), 3, oTime(1000, 0), nil, nil,
|
|
[]*oraclevm.Feed{oRichFeed()})),
|
|
|
|
// A nil []byte is `null` and an empty one is `""`. Both read back, and
|
|
// they are not the same block.
|
|
vec("O_BLOCK_OBS_NULL_VALUE", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{oNilValued()}, nil, nil)),
|
|
vec("O_BLOCK_OBS_EMPTY_VALUE", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{oEmptyValued()}, nil, nil)),
|
|
|
|
// A Go [32]byte reads a list of ANY length: the elements past the
|
|
// thirty-second are discarded without being type-checked, and the ones
|
|
// it never reached stay zero. Both are refusals a hand-written reader
|
|
// is likely to invent, and neither is one the chain makes.
|
|
vec("O_BLOCK_SOURCEMETA_SHORT", "O", "block",
|
|
oSourceMeta(obs1, "[1,2,3]")),
|
|
vec("O_BLOCK_SOURCEMETA_LONG", "O", "block",
|
|
oSourceMeta(obs1, oNumbers(40))),
|
|
|
|
// The block the chain WROTE, id and all. Its id is a different id.
|
|
vec("O_BLOCK_ID_SET", "O", "block", withID),
|
|
|
|
// A parent whose id renders as a chain LETTER rather than as CB58.
|
|
vec("O_BLOCK_NATIVE_PARENT", "O", "block",
|
|
oBlockWire(oNativeID('P'), 1, oTime(1000, 0), nil, nil, nil)),
|
|
|
|
// Sub-second time. Go trims trailing zeros from the fraction, so this
|
|
// is "…:40.5Z" and not "…:40.500000000Z", and an implementation that
|
|
// writes the full nine digits derives a different id.
|
|
vec("O_BLOCK_TIME_HALF_SECOND", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 500000000), nil, nil, nil)),
|
|
vec("O_BLOCK_TIME_NANO", "O", "block",
|
|
oBlockWire(oParent(), 1, oTime(1000, 123456789), nil, nil, nil)),
|
|
|
|
// A timestamp that is not in UTC. Go keeps the offset it was given
|
|
// rather than normalising, so the re-marshal — and the id — is over
|
|
// "+05:30" and not over the same instant written as Z.
|
|
vec("O_BLOCK_TIME_OFFSET", "O", "block",
|
|
[]byte(`{"id":"11111111111111111111111111111111LpoYY",`+
|
|
`"parentID":"`+oParent().String()+`",`+
|
|
`"height":1,"timestamp":"1970-01-01T05:46:40+05:30"}`)),
|
|
|
|
// `encoding/json` ignores a member the struct does not name. A reader
|
|
// that refuses unknown members refuses a block this chain accepts.
|
|
vec("O_BLOCK_UNKNOWN_MEMBER", "O", "block",
|
|
oInsert(empty, `"round":4,`)),
|
|
// Two members naming one field: the LAST one wins, silently. The
|
|
// pair below is what says WHICH, and neither alone would: with the
|
|
// intruder FIRST the original wins and the block is unchanged, with
|
|
// it LAST the block is a different block. A reader that took the
|
|
// first member disagrees on both rows and in opposite directions.
|
|
vec("O_BLOCK_DUPLICATE_HEIGHT_FIRST", "O", "block",
|
|
oInsert(empty, `"height":9,`)),
|
|
vec("O_BLOCK_DUPLICATE_HEIGHT_LAST", "O", "block",
|
|
oAppend(empty, `"height":9`)),
|
|
// A member name that differs from the field's tag only by case.
|
|
// `encoding/json` matches it, case-insensitively, when nothing matches
|
|
// exactly.
|
|
vec("O_BLOCK_FOLDED_MEMBER", "O", "block",
|
|
[]byte(strings.Replace(string(empty), `"height":`, `"HEIGHT":`, 1))),
|
|
// The exact name and the folded one, in the same object, with the
|
|
// FOLDED one last. Go's decoder walks the members in document order
|
|
// and decodes each one that resolves to the field, so exactness
|
|
// decides WHICH FIELD a name reaches and POSITION decides which value
|
|
// survives — a reader that preferred the exact member wherever it sat
|
|
// would keep 1 where the chain keeps 9. Nothing anywhere said which
|
|
// of those two rules the chain has until this vector asked.
|
|
vec("O_BLOCK_FOLDED_AFTER_EXACT", "O", "block",
|
|
oAppend(empty, `"HEIGHT":9`)),
|
|
vec("O_BLOCK_EXACT_AFTER_FOLDED", "O", "block",
|
|
oInsert(empty, `"HEIGHT":9,`)),
|
|
// null for a member leaves it at its zero value and is not an error.
|
|
vec("O_BLOCK_NULL_MEMBER", "O", "block",
|
|
oInsert(empty, `"observations":null,`)),
|
|
// An empty list parses, and then omitempty drops it on the way back
|
|
// out — so these bytes and O_BLOCK_EMPTY's are one id.
|
|
vec("O_BLOCK_EMPTY_ARRAY", "O", "block",
|
|
oInsert(empty, `"observations":[],`)),
|
|
|
|
// A number reaching an integer must BE one: Go hands the literal to
|
|
// strconv, so a float, an exponent, a negative and a value past the
|
|
// width are all refused, and the largest value that FITS is taken. The
|
|
// five are one rule read at five points, and a reader that ran the
|
|
// literal through a double would take three of them.
|
|
vec("O_BLOCK_HEIGHT_FLOAT", "O", "block", oHeight(empty, "1.0")),
|
|
vec("O_BLOCK_HEIGHT_EXPONENT", "O", "block", oHeight(empty, "1e2")),
|
|
vec("O_BLOCK_HEIGHT_NEGATIVE", "O", "block", oHeight(empty, "-1")),
|
|
vec("O_BLOCK_HEIGHT_MAX", "O", "block", oHeight(empty, "18446744073709551615")),
|
|
vec("O_BLOCK_HEIGHT_OVER_MAX", "O", "block", oHeight(empty, "18446744073709551616")),
|
|
// A scheme past the width of the byte it is read into.
|
|
vec("O_BLOCK_SCHEME_OVER_BYTE", "O", "block",
|
|
[]byte(strings.Replace(
|
|
string(oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs1}, nil, nil)),
|
|
`"scheme":1`, `"scheme":256`, 1))),
|
|
// A CB58 string whose checksum does not check.
|
|
vec("O_BLOCK_PARENT_BAD_CHECKSUM", "O", "block",
|
|
[]byte(strings.Replace(string(empty), oParent().String(),
|
|
oCorrupt(oParent().String()), 1))),
|
|
// A byte slice that is not base64.
|
|
vec("O_BLOCK_VALUE_BAD_BASE64", "O", "block",
|
|
[]byte(strings.Replace(
|
|
string(oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs1}, nil, nil)),
|
|
`"value":"`+oB64(obs1.Value)+`"`, `"value":"!!!!"`, 1))),
|
|
// A timestamp that is not RFC 3339.
|
|
vec("O_BLOCK_TIME_NOT_RFC3339", "O", "block",
|
|
[]byte(strings.Replace(string(empty), `"1970-01-01T00:16:40Z"`,
|
|
`"the tenth of never"`, 1))),
|
|
|
|
vec("O_BLOCK_NOT_JSON", "O", "block", []byte("this is not a block")),
|
|
vec("O_BLOCK_EMPTY_WIRE", "O", "block", nil),
|
|
vec("O_BLOCK_ONE_BYTE", "O", "block", []byte{'{'}),
|
|
}
|
|
|
|
v = append(v, oRequestIDVectors()...)
|
|
v = append(v, oRequestVectors()...)
|
|
v = append(v, oCommitVectors()...)
|
|
v = append(v, oObservationVectors()...)
|
|
|
|
oAssert(v)
|
|
return v
|
|
}
|
|
|
|
// oHeight rewrites a block's height member with a literal, so the number rules
|
|
// are read against the same block every time.
|
|
func oHeight(wire []byte, literal string) []byte {
|
|
s := string(wire)
|
|
if !strings.Contains(s, `"height":1,`) {
|
|
panic("the height member is not written as these vectors assume")
|
|
}
|
|
return []byte(strings.Replace(s, `"height":1,`, `"height":`+literal+`,`, 1))
|
|
}
|
|
|
|
// oProved is an aggregation carrying the two proofs that are omitempty: with
|
|
// non-zero lengths both are written, and with zero lengths neither is.
|
|
func oProved(seed byte, proof, cert int) *oraclevm.AggregatedValue {
|
|
a := oAggregated(oFeed(), 2, seed)
|
|
a.AggProof = oBytes(seed+1, proof)
|
|
a.QuorumCert = oBytes(seed+2, cert)
|
|
return a
|
|
}
|
|
|
|
// oRichFeed carries every member of a Feed that has a rendering of its own.
|
|
// The metadata keys are given out of order because Marshal sorts them, and the
|
|
// description holds the three runes it escapes.
|
|
func oRichFeed() *oraclevm.Feed {
|
|
f := oFeedRecord(id(5), "btc-usd")
|
|
f.Description = "spot <b> & \"mid\" > 0"
|
|
f.Sources = []string{"a.invalid", "b.invalid"}
|
|
f.UpdateFreq = 1500 * time.Millisecond
|
|
f.Operators = []ids.NodeID{oOperator(), nodeID(2)}
|
|
f.Metadata = map[string]string{"z": "last", "a": "first", "m": "middle"}
|
|
copy(f.PolicyHash[:], oBytes(0x80, 32))
|
|
return f
|
|
}
|
|
|
|
func oNilValued() *oraclevm.Observation {
|
|
o := oObservation(oFeed(), oOperator(), oTime(1500, 0), 0x20)
|
|
o.Value = nil
|
|
o.Signature = nil
|
|
return o
|
|
}
|
|
|
|
func oEmptyValued() *oraclevm.Observation {
|
|
o := oObservation(oFeed(), oOperator(), oTime(1500, 0), 0x20)
|
|
o.Value = []byte{}
|
|
o.Signature = []byte{}
|
|
return o
|
|
}
|
|
|
|
// oSourceMeta replaces an observation's fixed-array member with a list of a
|
|
// different length, which is a thing the Go reader takes rather than refuses.
|
|
func oSourceMeta(obs *oraclevm.Observation, list string) []byte {
|
|
wire := oBlockWire(oParent(), 1, oTime(1000, 0),
|
|
[]*oraclevm.Observation{obs}, nil, nil)
|
|
want := `"sourceMetaHash":` + oNumbers(32)
|
|
if !strings.Contains(string(wire), want) {
|
|
panic("the source-meta member is not written as this vector assumes")
|
|
}
|
|
return []byte(strings.Replace(string(wire), want, `"sourceMetaHash":`+list, 1))
|
|
}
|
|
|
|
// oNumbers is the list Marshal writes for n bytes of the value obs1 carries.
|
|
func oNumbers(n int) string {
|
|
parts := make([]string, n)
|
|
for i := range parts {
|
|
parts[i] = "34" // the 0x22 every byte of that member holds
|
|
}
|
|
return "[" + strings.Join(parts, ",") + "]"
|
|
}
|
|
|
|
// oInsert puts a member at the front of a JSON object's member list, which is
|
|
// where a duplicate has to sit for "the last one wins" to be the thing under
|
|
// test.
|
|
func oInsert(wire []byte, member string) []byte {
|
|
return []byte("{" + member + string(wire)[1:])
|
|
}
|
|
|
|
// oAppend puts a member at the END of a JSON object's member list, which is
|
|
// where a duplicate has to sit for it to be the one that wins.
|
|
func oAppend(wire []byte, member string) []byte {
|
|
s := string(wire)
|
|
return []byte(s[:len(s)-1] + "," + member + "}")
|
|
}
|
|
|
|
func oB64(b []byte) string {
|
|
w, err := json.Marshal(b)
|
|
must(err)
|
|
var s string
|
|
must(json.Unmarshal(w, &s))
|
|
return s
|
|
}
|
|
|
|
// oCorrupt changes one character of a CB58 string to another base-58 digit, so
|
|
// the string is still well formed base 58 and only its checksum is wrong.
|
|
func oCorrupt(s string) string {
|
|
b := []byte(s)
|
|
if b[3] == 'A' {
|
|
b[3] = 'B'
|
|
} else {
|
|
b[3] = 'A'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The request id, which is the whole of what makes an O request deterministic.
|
|
//
|
|
// A request is minted on the P-chain and executed on O, and the two sides
|
|
// agree on WHICH request only because both derive the same 32 bytes from the
|
|
// same five arguments. An implementation that folded step and retry in the
|
|
// other order, or wrote them little-endian, would execute one chain's requests
|
|
// under another chain's names. There is no wire here: the arguments are the
|
|
// vector, pipe-separated, the way D's derivations are.
|
|
//
|
|
// requestid serviceHex | sessionHex | txHex | step | retry
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func oRequestIDVectors() []Vector {
|
|
svc, ses, tx := hexID32(0x11), hexID32(0x12), hexID32(0x13)
|
|
zero := hexID32(0)
|
|
return []Vector{
|
|
vec("O_REQUESTID_ZERO", "O", "requestid", dArgs(zero, zero, zero, "0", "0")),
|
|
vec("O_REQUESTID_BASE", "O", "requestid", dArgs(svc, ses, tx, "0", "0")),
|
|
vec("O_REQUESTID_STEP", "O", "requestid", dArgs(svc, ses, tx, "1", "0")),
|
|
vec("O_REQUESTID_RETRY", "O", "requestid", dArgs(svc, ses, tx, "0", "1")),
|
|
vec("O_REQUESTID_STEP_MAX", "O", "requestid", dArgs(svc, ses, tx, "4294967295", "0")),
|
|
vec("O_REQUESTID_RETRY_MAX", "O", "requestid", dArgs(svc, ses, tx, "0", "4294967295")),
|
|
// The three ids each moved on their own. Each must derive a different
|
|
// request id: one that ignored any of them would let two requests
|
|
// share a name, and a name is what the executors sign against.
|
|
vec("O_REQUESTID_OTHER_SERVICE", "O", "requestid", dArgs(hexID32(0x14), ses, tx, "0", "0")),
|
|
vec("O_REQUESTID_OTHER_SESSION", "O", "requestid", dArgs(svc, hexID32(0x14), tx, "0", "0")),
|
|
vec("O_REQUESTID_OTHER_TX", "O", "requestid", dArgs(svc, ses, hexID32(0x14), "0", "0")),
|
|
// The service and session ids swapped. The preimage concatenates them
|
|
// in one order and nothing separates them, so an implementation that
|
|
// wrote them the other way round derives THIS id for the base vector.
|
|
vec("O_REQUESTID_SWAPPED_IDS", "O", "requestid", dArgs(ses, svc, tx, "0", "0")),
|
|
vec("O_REQUESTID_BAD_HEX", "O", "requestid", dArgs("zz", ses, tx, "0", "0")),
|
|
vec("O_REQUESTID_STEP_OVERFLOW", "O", "requestid", dArgs(svc, ses, tx, "4294967296", "0")),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Requests, records and the commitment over them.
|
|
//
|
|
// A `request` vector is an OracleRequest as JSON, offered to a chain that
|
|
// holds nothing. A `commit` vector carries a request AND the records executed
|
|
// against it, and its answer is the Merkle root the chain commits — the one
|
|
// number a light client checks an oracle answer against, and the one an
|
|
// implementation can get wrong without ever disagreeing about a block.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// oRequest builds a request whose id is the one its own arguments derive, so
|
|
// that the deterministic-id rule passes and the rules under it are reachable.
|
|
func oRequest(kind oraclevm.RequestKind, execs []ids.NodeID) *oraclevm.OracleRequest {
|
|
req := &oraclevm.OracleRequest{
|
|
ServiceID: id(0x11),
|
|
SessionID: id(0x12),
|
|
TxID: id(0x13),
|
|
Step: 0,
|
|
Retry: 0,
|
|
Kind: kind,
|
|
Target: oBytes(0x21, 8),
|
|
DeadlineHeight: 100,
|
|
Executors: execs,
|
|
CreatedAt: oTime(1000, 0),
|
|
}
|
|
copy(req.PayloadHash[:], oBytes(0x22, 32))
|
|
copy(req.SchemaHash[:], oBytes(0x23, 32))
|
|
req.RequestID = oracle.ComputeRequestID(req.ServiceID, req.SessionID, req.TxID, req.Step, req.Retry)
|
|
return req
|
|
}
|
|
|
|
func oRecord(req *oraclevm.OracleRequest, exec ids.NodeID, ts uint64, seed byte) *oraclevm.OracleRecord {
|
|
r := &oraclevm.OracleRecord{
|
|
RequestID: req.RequestID,
|
|
Executor: exec,
|
|
Timestamp: ts,
|
|
Endpoint: "https://feed.invalid/v1/price",
|
|
ResultCode: 200,
|
|
ExternalRef: oBytes(seed, 8),
|
|
Scheme: 1,
|
|
Signature: oBytes(seed+1, 16),
|
|
}
|
|
copy(r.BodyHash[:], oBytes(seed+2, 32))
|
|
return r
|
|
}
|
|
|
|
func oJSON(v any) []byte {
|
|
w, err := json.Marshal(v)
|
|
must(err)
|
|
return w
|
|
}
|
|
|
|
// oRun is what a `commit` vector carries: a request and the records offered
|
|
// against it, in order.
|
|
type oRun struct {
|
|
Request *oraclevm.OracleRequest `json:"request"`
|
|
Records []*oraclevm.OracleRecord `json:"records"`
|
|
}
|
|
|
|
func oRequestVectors() []Vector {
|
|
ok := oRequest(0, []ids.NodeID{oOperator()})
|
|
wrong := oRequest(0, []ids.NodeID{oOperator()})
|
|
wrong.RequestID[0] ^= 0xFF
|
|
read := oRequest(1, []ids.NodeID{oOperator()})
|
|
stepped := oRequest(0, []ids.NodeID{oOperator()})
|
|
stepped.Step = 3
|
|
// The id is left as the step-0 derivation, so the request now names itself
|
|
// with a name its own arguments do not derive.
|
|
return []Vector{
|
|
vec("O_REQUEST_WRITE", "O", "request", oJSON(ok)),
|
|
vec("O_REQUEST_READ", "O", "request", oJSON(read)),
|
|
vec("O_REQUEST_WRONG_ID", "O", "request", oJSON(wrong)),
|
|
vec("O_REQUEST_STALE_STEP", "O", "request", oJSON(stepped)),
|
|
vec("O_REQUEST_NOT_JSON", "O", "request", []byte("{")),
|
|
}
|
|
}
|
|
|
|
func oCommitVectors() []Vector {
|
|
req := oRequest(0, []ids.NodeID{nodeID(1), nodeID(2), nodeID(3)})
|
|
r1 := oRecord(req, nodeID(1), 1000, 0x50)
|
|
r2 := oRecord(req, nodeID(2), 1100, 0x60)
|
|
r3 := oRecord(req, nodeID(3), 1200, 0x70)
|
|
|
|
run := func(rs ...*oraclevm.OracleRecord) []byte {
|
|
return oJSON(&oRun{Request: req, Records: rs})
|
|
}
|
|
|
|
unauth := oRecord(req, nodeID(9), 1000, 0x50)
|
|
|
|
// A record that differs from r1 in ONE field, one per field the leaf hash
|
|
// is taken over. Each must move the root: a leaf that left a field out
|
|
// would let an executor change it after the commitment.
|
|
other := func(f func(*oraclevm.OracleRecord)) *oraclevm.OracleRecord {
|
|
r := *r1
|
|
f(&r)
|
|
return &r
|
|
}
|
|
|
|
return []Vector{
|
|
vec("O_COMMIT_ONE", "O", "commit", run(r1)),
|
|
vec("O_COMMIT_TWO", "O", "commit", run(r1, r2)),
|
|
vec("O_COMMIT_THREE", "O", "commit", run(r1, r2, r3)),
|
|
// Three records and four, where the fourth repeats the third. The
|
|
// tree duplicates a lone last leaf to pair it, so these two DIFFERENT
|
|
// record sets commit to the SAME root — a light client shown either
|
|
// one cannot tell which was executed. The pair is the vector; a single
|
|
// root proves nothing.
|
|
vec("O_COMMIT_THREE_PADDED", "O", "commit", run(r1, r2, r3, r3)),
|
|
vec("O_COMMIT_ORDER_SWAPPED", "O", "commit", run(r2, r1)),
|
|
vec("O_COMMIT_OTHER_TIMESTAMP", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.Timestamp = 1001 }))),
|
|
vec("O_COMMIT_OTHER_ENDPOINT", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.Endpoint += "?x=1" }))),
|
|
vec("O_COMMIT_OTHER_BODY", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.BodyHash[0] ^= 0xFF }))),
|
|
vec("O_COMMIT_OTHER_CODE", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.ResultCode = 404 }))),
|
|
vec("O_COMMIT_OTHER_REF", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.ExternalRef = oBytes(0x51, 8) }))),
|
|
// The signature is NOT in the leaf. Two records that differ only there
|
|
// commit to one root, which is the chain's answer and has to be every
|
|
// implementation's.
|
|
vec("O_COMMIT_OTHER_SIGNATURE", "O", "commit",
|
|
run(other(func(r *oraclevm.OracleRecord) { r.Signature = oBytes(0x99, 16) }))),
|
|
vec("O_COMMIT_NO_RECORDS", "O", "commit", run()),
|
|
vec("O_COMMIT_UNAUTHORIZED", "O", "commit", run(unauth)),
|
|
vec("O_COMMIT_NOT_JSON", "O", "commit", []byte("[]")),
|
|
}
|
|
}
|
|
|
|
func oObservationVectors() []Vector {
|
|
fresh := oObservation(oFeed(), oOperator(), oFresh(), 0x20)
|
|
stale := oObservation(oFeed(), oOperator(), oTime(1500, 0), 0x20)
|
|
unknownFeed := oObservation(id(0x66), oOperator(), oFresh(), 0x20)
|
|
unauthorized := oObservation(oFeed(), nodeID(8), oFresh(), 0x20)
|
|
return []Vector{
|
|
vec("O_OBS_ACCEPTED", "O", "observation", oJSON(fresh)),
|
|
vec("O_OBS_STALE", "O", "observation", oJSON(stale)),
|
|
vec("O_OBS_UNKNOWN_FEED", "O", "observation", oJSON(unknownFeed)),
|
|
vec("O_OBS_UNAUTHORIZED", "O", "observation", oJSON(unauthorized)),
|
|
vec("O_OBS_NOT_JSON", "O", "observation", []byte("7")),
|
|
}
|
|
}
|
|
|
|
// oAssert holds the emit to the claims this half of the corpus rests on. A
|
|
// vector that does not do what its name says is worse than no vector: it is a
|
|
// row three implementations will agree about for the wrong reason.
|
|
func oAssert(v []Vector) {
|
|
by := map[string]Result{}
|
|
for _, vec := range v {
|
|
by[vec.ID] = evalO(vec)
|
|
}
|
|
ok := func(idStr string) Result {
|
|
r := by[idStr]
|
|
if r.Parse != "ok" {
|
|
panic(fmt.Sprintf("%s is not the wire the O-chain writes: %s", idStr, r.Note))
|
|
}
|
|
return r
|
|
}
|
|
same := func(a, b string) {
|
|
if ok(a).Hash != ok(b).Hash {
|
|
panic(fmt.Sprintf("%s and %s were expected to derive one id", a, b))
|
|
}
|
|
}
|
|
differ := func(a, b string) {
|
|
if ok(a).Hash == ok(b).Hash {
|
|
panic(fmt.Sprintf("%s and %s were expected to derive different ids", a, b))
|
|
}
|
|
}
|
|
|
|
// The two id claims in the file header.
|
|
same("O_BLOCK_EMPTY", "O_BLOCK_EMPTY_ARRAY")
|
|
differ("O_BLOCK_EMPTY", "O_BLOCK_ID_SET")
|
|
written := oBlockID(oBlockWire(oParent(), 1, oTime(1000, 0), nil, nil, nil))
|
|
if by["O_BLOCK_ID_SET"].Hash == hex.EncodeToString(written[:]) {
|
|
panic("O_BLOCK_ID_SET was expected NOT to round-trip its own id")
|
|
}
|
|
|
|
// The five number vectors: four refusals and the one that fits.
|
|
for _, refused := range []string{
|
|
"O_BLOCK_HEIGHT_FLOAT", "O_BLOCK_HEIGHT_EXPONENT", "O_BLOCK_HEIGHT_NEGATIVE",
|
|
"O_BLOCK_HEIGHT_OVER_MAX", "O_BLOCK_SCHEME_OVER_BYTE",
|
|
} {
|
|
if by[refused].Parse != VMalformed {
|
|
panic(refused + " was expected to be refused, and was not: " + by[refused].Note)
|
|
}
|
|
}
|
|
if by["O_BLOCK_HEIGHT_MAX"].Parse != "ok" {
|
|
panic("the largest uint64 was refused: " + by["O_BLOCK_HEIGHT_MAX"].Note)
|
|
}
|
|
|
|
// omitempty writes the proofs that carry bytes and omits the empty ones,
|
|
// so the two aggregation blocks are two different blocks.
|
|
differ("O_BLOCK_AGG_PROOFS", "O_BLOCK_AGG_EMPTY_PROOF")
|
|
// A nil []byte and an empty one are `null` and `""`, and not one block.
|
|
differ("O_BLOCK_OBS_NULL_VALUE", "O_BLOCK_OBS_EMPTY_VALUE")
|
|
// A short fixed-array list zero-fills what it did not reach, and a long
|
|
// one discards the rest — so the short one is a DIFFERENT block and the
|
|
// long one is the SAME block as the one it was cut from.
|
|
differ("O_BLOCK_OBSERVATION", "O_BLOCK_SOURCEMETA_SHORT")
|
|
same("O_BLOCK_OBSERVATION", "O_BLOCK_SOURCEMETA_LONG")
|
|
|
|
// The unknown member is ignored, so it is the same block.
|
|
same("O_BLOCK_EMPTY", "O_BLOCK_UNKNOWN_MEMBER")
|
|
// The last of two members naming one field wins, so the intruder placed
|
|
// first changes nothing and the one placed last changes the block.
|
|
same("O_BLOCK_EMPTY", "O_BLOCK_DUPLICATE_HEIGHT_FIRST")
|
|
differ("O_BLOCK_EMPTY", "O_BLOCK_DUPLICATE_HEIGHT_LAST")
|
|
// A member name that matches only case-insensitively still reaches the
|
|
// field, so the block is unchanged.
|
|
same("O_BLOCK_EMPTY", "O_BLOCK_FOLDED_MEMBER")
|
|
// And position, not exactness, decides which of two names that both reach
|
|
// the field wins.
|
|
differ("O_BLOCK_EMPTY", "O_BLOCK_FOLDED_AFTER_EXACT")
|
|
same("O_BLOCK_EMPTY", "O_BLOCK_EXACT_AFTER_FOLDED")
|
|
// The native-chain parent must render through the alias table and NOT as
|
|
// CB58 — the alias carries no checksum, where every other id does — and it
|
|
// must read back as the id it names. Both halves are the vector: a port
|
|
// that only knows CB58 writes a different parent and derives a different
|
|
// id for this block, and one that writes the alias but cannot read it
|
|
// refuses the block outright.
|
|
native := oNativeID('P')
|
|
rendered := native.String()
|
|
if !strings.Contains(string(oBlockWire(native, 1, oTime(1000, 0), nil, nil, nil)),
|
|
`"parentID":"`+rendered+`"`) {
|
|
panic("O_BLOCK_NATIVE_PARENT does not exercise the native-chain rendering")
|
|
}
|
|
if rendered == ids.Empty.String() || len(rendered) != 33 {
|
|
panic("the native-chain alias is not the unchecksummed form this vector is for: " + rendered)
|
|
}
|
|
if by["O_BLOCK_NATIVE_PARENT"].Parse != "ok" {
|
|
panic("the native-chain alias did not read back: " + by["O_BLOCK_NATIVE_PARENT"].Note)
|
|
}
|
|
|
|
// The five arguments of a request id each move it.
|
|
for _, pair := range [][2]string{
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_STEP"},
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_RETRY"},
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_OTHER_SERVICE"},
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_OTHER_SESSION"},
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_OTHER_TX"},
|
|
{"O_REQUESTID_BASE", "O_REQUESTID_SWAPPED_IDS"},
|
|
{"O_REQUESTID_STEP", "O_REQUESTID_RETRY"},
|
|
} {
|
|
differ(pair[0], pair[1])
|
|
}
|
|
|
|
// The malleability the tree has: an odd last leaf is paired with itself,
|
|
// so three records and those three with the last repeated are one root.
|
|
same("O_COMMIT_THREE", "O_COMMIT_THREE_PADDED")
|
|
// Order is in the root, and so is every field the leaf hashes.
|
|
differ("O_COMMIT_TWO", "O_COMMIT_ORDER_SWAPPED")
|
|
for _, other := range []string{
|
|
"O_COMMIT_OTHER_TIMESTAMP", "O_COMMIT_OTHER_ENDPOINT",
|
|
"O_COMMIT_OTHER_BODY", "O_COMMIT_OTHER_CODE", "O_COMMIT_OTHER_REF",
|
|
} {
|
|
differ("O_COMMIT_ONE", other)
|
|
}
|
|
// And the signature is not.
|
|
same("O_COMMIT_ONE", "O_COMMIT_OTHER_SIGNATURE")
|
|
|
|
// The four observation vectors must reach four different rules, or the
|
|
// staleness rule and the authorisation rule are one row.
|
|
for _, want := range [][2]string{
|
|
{"O_OBS_ACCEPTED", VOK},
|
|
{"O_OBS_STALE", VSyntactic},
|
|
{"O_OBS_UNKNOWN_FEED", VLedger},
|
|
{"O_OBS_UNAUTHORIZED", VAuth},
|
|
} {
|
|
if by[want[0]].Exec != want[1] {
|
|
panic(fmt.Sprintf("%s answered %s, not %s: %s",
|
|
want[0], by[want[0]].Exec, want[1], by[want[0]].Note))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Answering.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// oBlockID is the id this chain derives for a block on the wire: parse it,
|
|
// marshal it again, hash that. Not the hash of the bytes given.
|
|
func oBlockID(wire []byte) ids.ID {
|
|
blk := &oraclevm.Block{}
|
|
if err := json.Unmarshal(wire, blk); err != nil {
|
|
return ids.Empty
|
|
}
|
|
again, err := json.Marshal(blk)
|
|
must(err)
|
|
return ids.ID(sha256.Sum256(again))
|
|
}
|
|
|
|
// ovm is an O-chain over an empty database, seeded with the corpus's feeds.
|
|
// One per process. SubmitObservation appends to a pending list nothing else
|
|
// reads, so the vectors do not see each other.
|
|
var (
|
|
ovmOnce sync.Once
|
|
ovmChain *oraclevm.VM
|
|
ovmErr error
|
|
)
|
|
|
|
func ovm() (*oraclevm.VM, error) {
|
|
ovmOnce.Do(func() { ovmChain, ovmErr = startOvm(oGenesisCfg) })
|
|
return ovmChain, ovmErr
|
|
}
|
|
|
|
func startOvm(genesis string) (*oraclevm.VM, error) {
|
|
// Initialize announces itself on stdout, and stdout is the result stream
|
|
// the runner reads.
|
|
stdout := os.Stdout
|
|
os.Stdout = os.Stderr
|
|
defer func() { os.Stdout = stdout }()
|
|
|
|
vm := &oraclevm.VM{}
|
|
err := vm.Initialize(context.Background(), luxvm.Init{
|
|
Runtime: &runtime.Runtime{
|
|
NodeID: nodeID(1),
|
|
NetworkID: networkID,
|
|
ChainID: id(oChain),
|
|
Log: log.Noop(),
|
|
},
|
|
DB: memdb.New(),
|
|
Log: log.Noop(),
|
|
Genesis: []byte(genesis),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return vm, nil
|
|
}
|
|
|
|
func evalO(v Vector) Result {
|
|
switch v.Op {
|
|
case "block":
|
|
return evalOBlock(v)
|
|
case "genesis":
|
|
return evalOGenesis(v)
|
|
case "requestid":
|
|
return evalORequestID(v)
|
|
case "request":
|
|
return evalORequest(v)
|
|
case "commit":
|
|
return evalOCommit(v)
|
|
case "observation":
|
|
return evalOObservation(v)
|
|
}
|
|
return Result{ID: v.ID, Parse: VInternal, Kind: none, Hash: none,
|
|
Syntactic: VInternal, Exec: VInternal, Note: "unknown O op " + v.Op}
|
|
}
|
|
|
|
func oResult(v Vector) (Result, []byte, bool) {
|
|
r := Result{ID: v.ID, Kind: none, Hash: none, Syntactic: none, Exec: none}
|
|
b, ok := wireOf(v)
|
|
if !ok {
|
|
r.Parse = VInternal
|
|
r.Note = "corpus wire is not hex"
|
|
return r, nil, false
|
|
}
|
|
return r, b, true
|
|
}
|
|
|
|
func oMalformed(r Result, err error) Result {
|
|
r.Parse = VMalformed
|
|
r.Syntactic = VMalformed
|
|
r.Exec = VMalformed
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
|
|
// evalOBlock reads a block, names what it carries, and derives its id.
|
|
//
|
|
// `Verify` is the chain's, and it is unconditional — so both verdict fields
|
|
// are OK for everything that parses. That is what this chain decides about a
|
|
// block, and printing anything else here would be this file's opinion rather
|
|
// than the reference's.
|
|
func evalOBlock(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
vm, err := ovm()
|
|
if err != nil {
|
|
r.Parse = VInternal
|
|
r.Note = "the reference VM did not start: " + err.Error()
|
|
return r
|
|
}
|
|
blk, err := vm.ParseBlock(context.Background(), b)
|
|
if err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
r.Parse = "ok"
|
|
ob, isO := blk.(*oraclevm.Block)
|
|
if !isO {
|
|
r.Parse = VInternal
|
|
r.Note = "ParseBlock returned something that is not an O block"
|
|
return r
|
|
}
|
|
r.Kind = oKindName(ob)
|
|
blockID := blk.ID()
|
|
r.Hash = hex.EncodeToString(blockID[:])
|
|
if err := blk.Verify(context.Background()); err != nil {
|
|
class := classify(err)
|
|
r.Syntactic = class
|
|
r.Exec = class
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
r.Syntactic = VOK
|
|
r.Exec = VOK
|
|
r.Note = fmt.Sprintf("height=%d parent=%s obs=%d agg=%d feeds=%d",
|
|
ob.Height_, shortID(ob.ParentID_), len(ob.Observations),
|
|
len(ob.Aggregations), len(ob.FeedUpdates))
|
|
return r
|
|
}
|
|
|
|
// oKindName says what a block CARRIES. The O-chain has one block type, so
|
|
// naming the type would compare a constant against itself; which of the three
|
|
// lists a buffer decoded into is the thing the implementations have to agree
|
|
// about before any verdict about them means anything.
|
|
func oKindName(b *oraclevm.Block) string {
|
|
var parts []string
|
|
add := func(name string, n int) {
|
|
switch {
|
|
case n == 0:
|
|
case n == 1:
|
|
parts = append(parts, name)
|
|
default:
|
|
parts = append(parts, fmt.Sprintf("%sx%d", name, n))
|
|
}
|
|
}
|
|
add("Observation", len(b.Observations))
|
|
add("Aggregation", len(b.Aggregations))
|
|
add("FeedUpdate", len(b.FeedUpdates))
|
|
if len(parts) == 0 {
|
|
return "Empty"
|
|
}
|
|
return strings.Join(parts, "+")
|
|
}
|
|
|
|
// evalOGenesis reads the genesis the observation vectors are judged under and
|
|
// answers with what it parsed, so that "we applied the same configuration" is
|
|
// a compared field rather than an assumption under every row below it.
|
|
//
|
|
// The hash is over the canonical re-marshal of the parsed struct, and NOT over
|
|
// the id of the genesis block the chain would build from it: that id is a
|
|
// function of the machine's timezone (see the note at the top of this file)
|
|
// and would put a different answer in every column on every box.
|
|
func evalOGenesis(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
g := &oraclevm.Genesis{}
|
|
if err := json.Unmarshal(b, g); err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
r.Parse = "ok"
|
|
r.Kind = "Genesis"
|
|
again, err := json.Marshal(g)
|
|
must(err)
|
|
sum := sha256.Sum256(again)
|
|
r.Hash = hex.EncodeToString(sum[:])
|
|
r.Syntactic = fmt.Sprintf("feeds=%d", len(g.InitialFeeds))
|
|
r.Exec = VOK
|
|
r.Note = fmt.Sprintf("timestamp=%d message=%q", g.Timestamp, g.Message)
|
|
return r
|
|
}
|
|
|
|
func evalORequestID(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
f := strings.Split(string(b), "|")
|
|
if len(f) != 5 {
|
|
return oMalformed(r, fmt.Errorf("a request id takes five arguments, got %d", len(f)))
|
|
}
|
|
svc, e1 := oArgID(f[0])
|
|
ses, e2 := oArgID(f[1])
|
|
tx, e3 := oArgID(f[2])
|
|
step, e4 := strconv.ParseUint(f[3], 10, 32)
|
|
retry, e5 := strconv.ParseUint(f[4], 10, 32)
|
|
for _, err := range []error{e1, e2, e3, e4, e5} {
|
|
if err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
}
|
|
r.Parse = "ok"
|
|
r.Kind = "RequestID"
|
|
sum := oracle.ComputeRequestID(svc, ses, tx, uint32(step), uint32(retry))
|
|
r.Hash = hex.EncodeToString(sum[:])
|
|
r.Syntactic = VOK
|
|
r.Exec = VOK
|
|
r.Note = fmt.Sprintf("step=%d retry=%d", step, retry)
|
|
return r
|
|
}
|
|
|
|
func oArgID(s string) (ids.ID, error) {
|
|
b, err := hex.DecodeString(s)
|
|
if err != nil {
|
|
return ids.Empty, err
|
|
}
|
|
if len(b) != 32 {
|
|
return ids.Empty, fmt.Errorf("an id is 32 bytes, got %d", len(b))
|
|
}
|
|
return ids.ID(b), nil
|
|
}
|
|
|
|
// evalORequest offers a request to a chain that holds nothing.
|
|
//
|
|
// RegisterRequest checks the deterministic id BEFORE it looks in the request
|
|
// map, and that lookup is its first read of the chain — so a request whose id
|
|
// its own arguments do not derive is refused ahead of the store and answers
|
|
// both fields, and everything past it answers exec alone. That boundary is the
|
|
// corpus's, defined in conformance/README.md, and this is where the reference
|
|
// puts it.
|
|
func evalORequest(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
req := &oraclevm.OracleRequest{}
|
|
if err := json.Unmarshal(b, req); err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
r.Parse = "ok"
|
|
r.Kind = oRequestKindName(req.Kind)
|
|
r.Hash = hex.EncodeToString(req.RequestID[:])
|
|
|
|
vm, err := startOvm(oGenesisCfg)
|
|
if err != nil {
|
|
r.Parse = VInternal
|
|
r.Note = "the reference VM did not start: " + err.Error()
|
|
return r
|
|
}
|
|
if err := vm.RegisterRequest(req); err != nil {
|
|
class := classify(err)
|
|
if strings.HasPrefix(err.Error(), "invalid request_id") {
|
|
r.Syntactic = class
|
|
} else {
|
|
r.Syntactic = VOK
|
|
}
|
|
r.Exec = class
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
r.Syntactic = VOK
|
|
r.Exec = VOK
|
|
r.Note = fmt.Sprintf("executors=%d deadline=%d", len(req.Executors), req.DeadlineHeight)
|
|
return r
|
|
}
|
|
|
|
func oRequestKindName(k oraclevm.RequestKind) string {
|
|
switch k {
|
|
case 0:
|
|
return "Write"
|
|
case 1:
|
|
return "Read"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// evalOCommit runs a request and its records through the chain and answers
|
|
// with the Merkle root the chain commits to.
|
|
//
|
|
// Every refusal here is past the chain's first read — RegisterRequest's own
|
|
// deterministic-id check aside, which the request vectors already cover — so
|
|
// syntactic is OK and exec carries the class.
|
|
func evalOCommit(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
run := &oRun{}
|
|
if err := json.Unmarshal(b, run); err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
if run.Request == nil {
|
|
return oMalformed(r, fmt.Errorf("a commit run carries a request"))
|
|
}
|
|
r.Parse = "ok"
|
|
r.Kind = "OracleCommit"
|
|
r.Syntactic = fmt.Sprintf("records=%d", len(run.Records))
|
|
|
|
vm, err := startOvm(oGenesisCfg)
|
|
if err != nil {
|
|
r.Parse = VInternal
|
|
r.Note = "the reference VM did not start: " + err.Error()
|
|
return r
|
|
}
|
|
if err := vm.RegisterRequest(run.Request); err != nil {
|
|
r.Exec = classify(err)
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
for _, rec := range run.Records {
|
|
if err := vm.SubmitRecord(rec); err != nil {
|
|
r.Exec = classify(err)
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
}
|
|
commit, err := vm.CommitRecords(run.Request.RequestID)
|
|
if err != nil {
|
|
r.Exec = classify(err)
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
r.Hash = hex.EncodeToString(commit.Root[:])
|
|
r.Exec = VOK
|
|
r.Note = fmt.Sprintf("count=%d window=[%d,%d]",
|
|
commit.RecordCount, commit.Window.Start, commit.Window.End)
|
|
return r
|
|
}
|
|
|
|
// evalOObservation offers an observation to the seeded chain. Its first act is
|
|
// a feed lookup, which is a read of the chain, so every refusal it can reach
|
|
// is exec and syntactic is OK.
|
|
func evalOObservation(v Vector) Result {
|
|
r, b, ok := oResult(v)
|
|
if !ok {
|
|
return r
|
|
}
|
|
obs := &oraclevm.Observation{}
|
|
if err := json.Unmarshal(b, obs); err != nil {
|
|
return oMalformed(r, err)
|
|
}
|
|
r.Parse = "ok"
|
|
r.Kind = "Observation"
|
|
r.Hash = hex.EncodeToString(obs.FeedID[:])
|
|
r.Syntactic = VOK
|
|
|
|
vm, err := ovm()
|
|
if err != nil {
|
|
r.Parse = VInternal
|
|
r.Note = "the reference VM did not start: " + err.Error()
|
|
return r
|
|
}
|
|
if err := vm.SubmitObservation(obs); err != nil {
|
|
r.Exec = classify(err)
|
|
r.Note = trim(err.Error())
|
|
return r
|
|
}
|
|
r.Exec = VOK
|
|
r.Note = "accepted into the pending set"
|
|
return r
|
|
}
|