forked from TrueCloudLab/frostfs-contract
[#28] audit: Update audit contract
In updated audit scheme, contract stores marshalled audit result structure and provides interface to list them by epoch, container or exact audit executor. Signed-off-by: Alex Vanin <alexey@nspcc.ru>
This commit is contained in:
parent
c6fd4dc77c
commit
dbf1149f3a
1 changed files with 111 additions and 181 deletions
|
@ -1,8 +1,10 @@
|
||||||
package auditcontract
|
package auditcontract
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/binary"
|
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/crypto"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||||
|
@ -10,52 +12,40 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
irNode struct {
|
irNode struct {
|
||||||
key []byte
|
key interop.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckedNode struct {
|
auditHeader struct {
|
||||||
Key []byte // 33 bytes
|
epoch int
|
||||||
Pair int // 2 bytes
|
cid []byte
|
||||||
Reward int // ? up to 32 byte
|
from interop.PublicKey
|
||||||
}
|
|
||||||
|
|
||||||
AuditResult struct {
|
|
||||||
InnerRingNode []byte // 33 bytes
|
|
||||||
Epoch int // 8 bytes
|
|
||||||
ContainerID []byte // 32 bytes
|
|
||||||
StorageGroupID []byte // 16 bytes
|
|
||||||
PoR bool // 1 byte
|
|
||||||
PDP bool // 1 byte
|
|
||||||
// --- 91 bytes -- //
|
|
||||||
// --- 2 more bytes to size of the []CheckedNode //
|
|
||||||
Nodes []CheckedNode // <= 67 bytes per node
|
|
||||||
// about 1400 nodes may be presented in container
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Audit key is a combination of epoch, container ID and public key of node that
|
||||||
|
// executed audit. Together it should be no more than 64 bytes. We can't shrink
|
||||||
|
// epoch and container ID since we iterate over these values. But we can shrink
|
||||||
|
// public key by using first bytes of the hashed value.
|
||||||
|
|
||||||
|
const maxKeySize = 24 // 24 + 32 (container ID length) + 8 (epoch length) = 64
|
||||||
|
|
||||||
|
func (a auditHeader) ID() []byte {
|
||||||
|
var buf interface{} = a.epoch
|
||||||
|
|
||||||
|
hashedKey := crypto.SHA256(a.from)
|
||||||
|
shortedKey := hashedKey[:maxKeySize]
|
||||||
|
|
||||||
|
return append(buf.([]byte), append(a.cid, shortedKey...)...)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
// 1E-8 GAS in precision of balance container.
|
netmapContractKey = "netmapScriptHash"
|
||||||
// This value may be calculated in runtime based on decimal value of
|
netmapContractKeyLn = len(netmapContractKey)
|
||||||
// balance contract. We can also provide methods to change fee
|
|
||||||
// in runtime.
|
|
||||||
auditFee = 1 * 100_000_000
|
|
||||||
|
|
||||||
ownerIDLength = 25
|
|
||||||
|
|
||||||
journalKey = "auditJournal"
|
|
||||||
balanceContractKey = "balanceScriptHash"
|
|
||||||
containerContractKey = "containerScriptHash"
|
|
||||||
netmapContractKey = "netmapScriptHash"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ctx storage.Context
|
||||||
auditFeeTransferMsg = []byte("audit execution fee")
|
|
||||||
auditRewardTransferMsg = []byte("data audit reward")
|
|
||||||
|
|
||||||
ctx storage.Context
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if runtime.GetTrigger() != runtime.Application {
|
if runtime.GetTrigger() != runtime.Application {
|
||||||
|
@ -65,22 +55,16 @@ func init() {
|
||||||
ctx = storage.GetContext()
|
ctx = storage.GetContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(addrNetmap, addrBalance, addrContainer []byte) {
|
func Init(addrNetmap interop.Hash160) {
|
||||||
if storage.Get(ctx, netmapContractKey) != nil &&
|
if storage.Get(ctx, netmapContractKey) != nil {
|
||||||
storage.Get(ctx, balanceContractKey) != nil &&
|
|
||||||
storage.Get(ctx, containerContractKey) != nil {
|
|
||||||
panic("init: contract already deployed")
|
panic("init: contract already deployed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(addrNetmap) != 20 || len(addrBalance) != 20 || len(addrContainer) != 20 {
|
if len(addrNetmap) != 20 {
|
||||||
panic("init: incorrect length of contract script hash")
|
panic("init: incorrect length of contract script hash")
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.Put(ctx, netmapContractKey, addrNetmap)
|
storage.Put(ctx, netmapContractKey, addrNetmap)
|
||||||
storage.Put(ctx, balanceContractKey, addrBalance)
|
|
||||||
storage.Put(ctx, containerContractKey, addrContainer)
|
|
||||||
|
|
||||||
setSerialized(ctx, journalKey, []AuditResult{})
|
|
||||||
|
|
||||||
runtime.Log("audit contract initialized")
|
runtime.Log("audit contract initialized")
|
||||||
}
|
}
|
||||||
|
@ -89,164 +73,110 @@ func Put(rawAuditResult []byte) bool {
|
||||||
netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte)
|
netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte)
|
||||||
innerRing := contract.Call(netmapContractAddr, "innerRingList").([]irNode)
|
innerRing := contract.Call(netmapContractAddr, "innerRingList").([]irNode)
|
||||||
|
|
||||||
auditResult, err := newAuditResult(rawAuditResult)
|
hdr := newAuditHeader(rawAuditResult)
|
||||||
if err {
|
presented := false
|
||||||
panic("put: can't parse audit result")
|
|
||||||
}
|
|
||||||
|
|
||||||
var presented = false
|
|
||||||
|
|
||||||
for i := range innerRing {
|
for i := range innerRing {
|
||||||
ir := innerRing[i]
|
ir := innerRing[i]
|
||||||
if bytesEqual(ir.key, auditResult.InnerRingNode) {
|
if bytesEqual(ir.key, hdr.from) {
|
||||||
presented = true
|
presented = true
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !runtime.CheckWitness(auditResult.InnerRingNode) || !presented {
|
if !runtime.CheckWitness(hdr.from) || !presented {
|
||||||
panic("put: access denied")
|
panic("audit: put access denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: limit size of the audit journal:
|
storage.Put(ctx, hdr.ID(), rawAuditResult)
|
||||||
// history will be stored in chain (args or notifies)
|
|
||||||
// contract storage will be used as a cache if needed
|
|
||||||
journal := getAuditResult(ctx)
|
|
||||||
journal = append(journal, auditResult)
|
|
||||||
|
|
||||||
setSerialized(ctx, journalKey, journal)
|
runtime.Log("audit: result has been saved")
|
||||||
|
|
||||||
if auditResult.PDP && auditResult.PoR {
|
|
||||||
// find who is the ownerID
|
|
||||||
containerContract := storage.Get(ctx, containerContractKey).([]byte)
|
|
||||||
|
|
||||||
// todo: implement easy way to get owner from the container id
|
|
||||||
ownerID := contract.Call(containerContract, "owner", auditResult.ContainerID).([]byte)
|
|
||||||
if len(ownerID) != ownerIDLength {
|
|
||||||
runtime.Log("put: can't get owner id of the container")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ownerScriptHash := walletToScripHash(ownerID)
|
|
||||||
|
|
||||||
// transfer fee to the inner ring node
|
|
||||||
balanceContract := storage.Get(ctx, balanceContractKey).([]byte)
|
|
||||||
irScriptHash := contract.CreateStandardAccount(auditResult.InnerRingNode)
|
|
||||||
|
|
||||||
tx := contract.Call(balanceContract, "transferX",
|
|
||||||
ownerScriptHash,
|
|
||||||
irScriptHash,
|
|
||||||
auditFee,
|
|
||||||
auditFeeTransferMsg, // todo: add epoch, container and storage group info
|
|
||||||
)
|
|
||||||
if !tx.(bool) {
|
|
||||||
panic("put: can't transfer inner ring fee")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(auditResult.Nodes); i++ {
|
|
||||||
node := auditResult.Nodes[i]
|
|
||||||
nodeScriptHash := contract.CreateStandardAccount(node.Key)
|
|
||||||
|
|
||||||
tx := contract.Call(balanceContract, "transferX",
|
|
||||||
ownerScriptHash,
|
|
||||||
nodeScriptHash,
|
|
||||||
node.Reward,
|
|
||||||
auditRewardTransferMsg, // todo: add epoch, container and storage group info
|
|
||||||
)
|
|
||||||
if !tx.(bool) {
|
|
||||||
runtime.Log("put: can't transfer storage payment")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Get(id []byte) []byte {
|
||||||
|
return storage.Get(ctx, id).([]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
func List() [][]byte {
|
||||||
|
it := storage.Find(ctx, []byte{})
|
||||||
|
|
||||||
|
return list(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListByEpoch(epoch int) [][]byte {
|
||||||
|
it := storage.Find(ctx, epoch)
|
||||||
|
|
||||||
|
return list(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListByCID(epoch int, cid []byte) [][]byte {
|
||||||
|
var buf interface{} = epoch
|
||||||
|
|
||||||
|
prefix := append(buf.([]byte), cid...)
|
||||||
|
it := storage.Find(ctx, prefix)
|
||||||
|
|
||||||
|
return list(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListByNode(epoch int, cid []byte, key interop.PublicKey) [][]byte {
|
||||||
|
hdr := auditHeader{
|
||||||
|
epoch: epoch,
|
||||||
|
cid: cid,
|
||||||
|
from: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
it := storage.Find(ctx, hdr.ID())
|
||||||
|
|
||||||
|
return list(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(it iterator.Iterator) [][]byte {
|
||||||
|
var result [][]byte
|
||||||
|
|
||||||
|
for iterator.Next(it) {
|
||||||
|
key := iterator.Key(it).([]byte)
|
||||||
|
if len(key) == netmapContractKeyLn {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func Version() int {
|
func Version() int {
|
||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuditResult(data []byte) (AuditResult, bool) {
|
// readNext reads length from first byte and then reads data (max 127 bytes).
|
||||||
var (
|
func readNext(input []byte) ([]byte, int) {
|
||||||
tmp interface{}
|
var buf interface{} = input[0]
|
||||||
ln = len(data)
|
ln := buf.(int)
|
||||||
result = AuditResult{
|
|
||||||
InnerRingNode: nil, // neo-go#949
|
|
||||||
ContainerID: nil,
|
|
||||||
StorageGroupID: nil,
|
|
||||||
Nodes: []CheckedNode{},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(data) < 91 { // all required headers
|
return input[1 : 1+ln], 1 + ln
|
||||||
runtime.Log("newAuditResult: can't parse audit result header")
|
|
||||||
return result, true
|
|
||||||
}
|
|
||||||
|
|
||||||
result.InnerRingNode = data[0:33]
|
|
||||||
|
|
||||||
epoch := data[33:41]
|
|
||||||
tmp = epoch
|
|
||||||
result.Epoch = tmp.(int)
|
|
||||||
|
|
||||||
result.ContainerID = data[41:73]
|
|
||||||
result.StorageGroupID = data[73:89]
|
|
||||||
result.PoR = util.Equals(data[90], 0x01)
|
|
||||||
result.PDP = util.Equals(data[91], 0x01)
|
|
||||||
|
|
||||||
// if there are nodes, that were checked
|
|
||||||
if len(data) > 93 {
|
|
||||||
rawCounter := data[91:93]
|
|
||||||
tmp = rawCounter
|
|
||||||
counter := tmp.(int)
|
|
||||||
|
|
||||||
ptr := 93
|
|
||||||
|
|
||||||
for i := 0; i < counter; i++ {
|
|
||||||
if ptr+33+2+32 > ln {
|
|
||||||
runtime.Log("newAuditResult: broken node")
|
|
||||||
return result, false
|
|
||||||
}
|
|
||||||
|
|
||||||
node := CheckedNode{
|
|
||||||
Key: nil, // neo-go#949
|
|
||||||
}
|
|
||||||
node.Key = data[ptr : ptr+33]
|
|
||||||
|
|
||||||
pair := data[ptr+33 : ptr+35]
|
|
||||||
tmp = pair
|
|
||||||
node.Pair = tmp.(int)
|
|
||||||
|
|
||||||
reward := data[ptr+35 : ptr+67]
|
|
||||||
tmp = reward
|
|
||||||
node.Reward = tmp.(int)
|
|
||||||
|
|
||||||
result.Nodes = append(result.Nodes, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuditResult(ctx storage.Context) []AuditResult {
|
func newAuditHeader(input []byte) auditHeader {
|
||||||
data := storage.Get(ctx, journalKey)
|
var buf interface{} = input[1:9] // [ epoch wireType (1 byte), 8 integer bytes ]
|
||||||
if data != nil {
|
epoch := buf.(int)
|
||||||
return binary.Deserialize(data.([]byte)).([]AuditResult)
|
|
||||||
|
// cid is a nested structure with raw bytes
|
||||||
|
// [ cid struct prefix (wireType + len = 2 bytes), cid value wireType (1 byte), ... ]
|
||||||
|
cid, offset := readNext(input[9+2+1:])
|
||||||
|
|
||||||
|
// key is a raw byte
|
||||||
|
// [ public key wireType (1 byte), ... ]
|
||||||
|
key, _ := readNext(input[9+2+1+offset+1:])
|
||||||
|
|
||||||
|
return auditHeader{
|
||||||
|
epoch,
|
||||||
|
cid,
|
||||||
|
key,
|
||||||
}
|
}
|
||||||
|
|
||||||
return []AuditResult{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setSerialized(ctx storage.Context, key interface{}, value interface{}) {
|
|
||||||
data := binary.Serialize(value)
|
|
||||||
storage.Put(ctx, key, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func walletToScripHash(wallet []byte) []byte {
|
|
||||||
return wallet[1 : len(wallet)-4]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// neo-go#1176
|
// neo-go#1176
|
||||||
|
|
Loading…
Reference in a new issue