frostfs-contract/audit/audit_contract.go

257 lines
6.2 KiB
Go

package auditcontract
import (
"github.com/nspcc-dev/neo-go/pkg/interop/binary"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/engine"
"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/util"
)
type (
irNode struct {
key []byte
}
CheckedNode struct {
Key []byte // 33 bytes
Pair int // 2 bytes
Reward int // ? up to 32 byte
}
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
}
)
const (
version = 1
// 1E-8 GAS in precision of balance container.
// This value may be calculated in runtime based on decimal value of
// 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 (
auditFeeTransferMsg = []byte("audit execution fee")
auditRewardTransferMsg = []byte("data audit reward")
ctx storage.Context
)
func init() {
if runtime.GetTrigger() != runtime.Application {
panic("contract has not been called in application node")
}
ctx = storage.GetContext()
}
func Init(addrNetmap, addrBalance, addrContainer []byte) {
if storage.Get(ctx, netmapContractKey) != nil &&
storage.Get(ctx, balanceContractKey) != nil &&
storage.Get(ctx, containerContractKey) != nil {
panic("init: contract already deployed")
}
if len(addrNetmap) != 20 || len(addrBalance) != 20 || len(addrContainer) != 20 {
panic("init: incorrect length of contract script hash")
}
storage.Put(ctx, netmapContractKey, addrNetmap)
storage.Put(ctx, balanceContractKey, addrBalance)
storage.Put(ctx, containerContractKey, addrContainer)
setSerialized(ctx, journalKey, []AuditResult{})
runtime.Log("audit contract initialized")
}
func Put(rawAuditResult []byte) bool {
netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte)
innerRing := engine.AppCall(netmapContractAddr, "innerRingList").([]irNode)
auditResult, err := newAuditResult(rawAuditResult)
if err {
panic("put: can't parse audit result")
}
var presented = false
for i := range innerRing {
ir := innerRing[i]
if bytesEqual(ir.key, auditResult.InnerRingNode) {
presented = true
break
}
}
if !runtime.CheckWitness(auditResult.InnerRingNode) || !presented {
panic("put: access denied")
}
// todo: limit size of the audit journal:
// 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)
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 := engine.AppCall(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 := engine.AppCall(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 := engine.AppCall(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
}
func Version() int {
return version
}
func newAuditResult(data []byte) (AuditResult, bool) {
var (
tmp interface{}
ln = len(data)
result = AuditResult{
InnerRingNode: nil, // neo-go#949
ContainerID: nil,
StorageGroupID: nil,
Nodes: []CheckedNode{},
}
)
if len(data) < 91 { // all required headers
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 {
data := storage.Get(ctx, journalKey)
if data != nil {
return binary.Deserialize(data.([]byte)).([]AuditResult)
}
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
func bytesEqual(a []byte, b []byte) bool {
return util.Equals(string(a), string(b))
}