tutus-chain/pkg/core/native/palam.go

1128 lines
33 KiB
Go

package native
import (
"encoding/binary"
"errors"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/config"
"github.com/tutus-one/tutus-chain/pkg/core/dao"
"github.com/tutus-one/tutus-chain/pkg/core/interop"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/core/state"
"github.com/tutus-one/tutus-chain/pkg/core/storage"
"github.com/tutus-one/tutus-chain/pkg/crypto/hash"
"github.com/tutus-one/tutus-chain/pkg/smartcontract"
"github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
"github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// Palam represents the programmed transparency native contract.
// Palam (Latin for "openly/publicly") provides role-based encrypted
// transaction flows with judicial declassification.
type Palam struct {
interop.ContractMD
Annos IAnnos
Vita IVita
RoleRegistry IRoleRegistry
Lex ILex
}
// PalamCache represents the cached state for Palam contract.
type PalamCache struct {
flowCount uint64
requestCount uint64
logCount uint64
attachmentCount uint64
}
// Storage key prefixes for Palam.
const (
palamPrefixFlow byte = 0x01 // flowID -> Flow
palamPrefixFlowByBucket byte = 0x02 // bucket + flowID -> exists
palamPrefixFlowByParticipant byte = 0x03 // participant + flowID -> exists
palamPrefixFlowByTag byte = 0x04 // tag + flowID -> exists
palamPrefixRequest byte = 0x10 // requestID -> DeclassifyRequest
palamPrefixRequestByFlow byte = 0x11 // flowID + requestID -> exists
palamPrefixRequestByRequester byte = 0x12 // requester + requestID -> exists
palamPrefixPendingRequest byte = 0x13 // flowID -> pending requestID
palamPrefixAccessLog byte = 0x20 // logID -> AccessLog
palamPrefixLogByFlow byte = 0x21 // flowID + logID -> exists
palamPrefixLogByAccessor byte = 0x22 // accessor + logID -> exists
palamPrefixAttachment byte = 0x30 // attachmentID -> FlowAttachment
palamPrefixAttachmentByFlow byte = 0x31 // flowID + attachmentID -> exists
palamPrefixFlowCounter byte = 0xF0 // -> uint64
palamPrefixRequestCounter byte = 0xF1 // -> next request ID
palamPrefixLogCounter byte = 0xF2 // -> next log ID
palamPrefixAttachmentCounter byte = 0xF3 // -> next attachment ID
palamPrefixConfig byte = 0xFF // -> PalamConfig
)
// Event names for Palam.
const (
FlowRecordedEvent = "FlowRecorded"
FlowAttachmentEvent = "FlowAttachment"
FlowAccessedEvent = "FlowAccessed"
DeclassifyRequestedEvent = "DeclassifyRequested"
DeclassifyApprovalEvent = "DeclassifyApproval"
DeclassifyGrantedEvent = "DeclassifyGranted"
DeclassifyDeniedEvent = "DeclassifyDenied"
DeclassifyExpiredEvent = "DeclassifyExpired"
)
// Role constants for Palam (from RoleRegistry).
const (
RolePalamAuditor uint64 = 25 // Can request declassification
RolePalamJudge uint64 = 26 // Can approve declassification
)
// Various errors for Palam.
var (
ErrPalamFlowNotFound = errors.New("flow not found")
ErrPalamFlowExists = errors.New("flow already exists")
ErrPalamRequestNotFound = errors.New("declassify request not found")
ErrPalamRequestExists = errors.New("declassify request already exists")
ErrPalamNotParticipant = errors.New("caller is not a participant")
ErrPalamNotAuditor = errors.New("caller is not an auditor")
ErrPalamNotJudge = errors.New("caller is not a judge")
ErrPalamAlreadyApproved = errors.New("already approved this request")
ErrPalamRequestExpired = errors.New("request has expired")
ErrPalamRequestNotPending = errors.New("request is not pending")
ErrPalamRequestDenied = errors.New("request was denied")
ErrPalamNoVita = errors.New("caller must have an active Vita")
ErrPalamInvalidTag = errors.New("invalid tag")
ErrPalamInvalidBucket = errors.New("invalid bucket")
ErrPalamInvalidCaseID = errors.New("invalid case ID")
ErrPalamInvalidReason = errors.New("invalid reason (minimum 50 characters)")
ErrPalamCannotSelfApprove = errors.New("cannot approve own request")
ErrPalamNoPermission = errors.New("no permission for this action")
ErrPalamInvalidAttachment = errors.New("invalid attachment type")
ErrPalamAttachmentNotFound = errors.New("attachment not found")
ErrPalamNotCommittee = errors.New("invalid committee signature")
)
// NewPalam creates a new Palam native contract.
func NewPalam() *Palam {
p := &Palam{}
p.ContractMD = *interop.NewContractMD(nativenames.Palam, nativeids.Palam)
defer p.BuildHFSpecificMD(p.ActiveIn())
desc := NewDescriptor("getConfig", smartcontract.ArrayType)
md := NewMethodAndPrice(p.getConfig, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
// ===== Flow Recording =====
desc = NewDescriptor("recordFlow", smartcontract.Hash256Type,
manifest.NewParameter("tag", smartcontract.StringType),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("bucket", smartcontract.StringType),
manifest.NewParameter("participants", smartcontract.ArrayType),
manifest.NewParameter("consumerData", smartcontract.ByteArrayType),
manifest.NewParameter("merchantData", smartcontract.ByteArrayType),
manifest.NewParameter("distributorData", smartcontract.ByteArrayType),
manifest.NewParameter("producerData", smartcontract.ByteArrayType),
manifest.NewParameter("ngoData", smartcontract.ByteArrayType),
manifest.NewParameter("auditorData", smartcontract.ByteArrayType),
manifest.NewParameter("previousFlowID", smartcontract.Hash256Type))
md = NewMethodAndPrice(p.recordFlow, 1<<17, callflag.States|callflag.AllowNotify)
p.AddMethod(md, desc)
desc = NewDescriptor("attachToFlow", smartcontract.IntegerType,
manifest.NewParameter("flowID", smartcontract.Hash256Type),
manifest.NewParameter("attachmentType", smartcontract.StringType),
manifest.NewParameter("encryptedData", smartcontract.ByteArrayType))
md = NewMethodAndPrice(p.attachToFlow, 1<<16, callflag.States|callflag.AllowNotify)
p.AddMethod(md, desc)
// ===== Flow Queries =====
desc = NewDescriptor("getFlow", smartcontract.ArrayType,
manifest.NewParameter("flowID", smartcontract.Hash256Type))
md = NewMethodAndPrice(p.getFlow, 1<<15, callflag.ReadStates|callflag.AllowNotify)
p.AddMethod(md, desc)
desc = NewDescriptor("getTotalFlows", smartcontract.IntegerType)
md = NewMethodAndPrice(p.getTotalFlows, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = NewDescriptor("getAttachment", smartcontract.ArrayType,
manifest.NewParameter("attachmentID", smartcontract.IntegerType))
md = NewMethodAndPrice(p.getAttachment, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = NewDescriptor("getTotalAttachments", smartcontract.IntegerType)
md = NewMethodAndPrice(p.getTotalAttachments, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
// ===== Declassification =====
desc = NewDescriptor("requestDeclassify", smartcontract.IntegerType,
manifest.NewParameter("flowID", smartcontract.Hash256Type),
manifest.NewParameter("caseID", smartcontract.StringType),
manifest.NewParameter("reason", smartcontract.StringType),
manifest.NewParameter("expirationBlocks", smartcontract.IntegerType))
md = NewMethodAndPrice(p.requestDeclassify, 1<<17, callflag.States|callflag.AllowNotify)
p.AddMethod(md, desc)
desc = NewDescriptor("approveDeclassify", smartcontract.BoolType,
manifest.NewParameter("requestID", smartcontract.IntegerType))
md = NewMethodAndPrice(p.approveDeclassify, 1<<16, callflag.States|callflag.AllowNotify)
p.AddMethod(md, desc)
desc = NewDescriptor("denyDeclassify", smartcontract.BoolType,
manifest.NewParameter("requestID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(p.denyDeclassify, 1<<16, callflag.States|callflag.AllowNotify)
p.AddMethod(md, desc)
desc = NewDescriptor("getDeclassifyRequest", smartcontract.ArrayType,
manifest.NewParameter("requestID", smartcontract.IntegerType))
md = NewMethodAndPrice(p.getDeclassifyRequest, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = NewDescriptor("getTotalRequests", smartcontract.IntegerType)
md = NewMethodAndPrice(p.getTotalRequests, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = NewDescriptor("hasDeclassifyGrant", smartcontract.BoolType,
manifest.NewParameter("flowID", smartcontract.Hash256Type),
manifest.NewParameter("requester", smartcontract.Hash160Type))
md = NewMethodAndPrice(p.hasDeclassifyGrant, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
// ===== Access Logging =====
desc = NewDescriptor("getAccessLog", smartcontract.ArrayType,
manifest.NewParameter("logID", smartcontract.IntegerType))
md = NewMethodAndPrice(p.getAccessLog, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = NewDescriptor("getTotalLogs", smartcontract.IntegerType)
md = NewMethodAndPrice(p.getTotalLogs, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
// ===== Role & Permissions =====
desc = NewDescriptor("getRolePermissions", smartcontract.ArrayType,
manifest.NewParameter("role", smartcontract.IntegerType))
md = NewMethodAndPrice(p.getRolePermissions, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
return p
}
// Metadata returns the metadata of the contract.
func (p *Palam) Metadata() *interop.ContractMD {
return &p.ContractMD
}
// Initialize initializes the Palam contract.
func (p *Palam) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
if hf != p.ActiveIn() {
return nil
}
// Initialize counters
p.setFlowCounter(ic.DAO, 0)
p.setRequestCounter(ic.DAO, 0)
p.setLogCounter(ic.DAO, 0)
p.setAttachmentCounter(ic.DAO, 0)
// Set default config
cfg := state.DefaultPalamConfig()
cfgItem := cfg.ToStackItem()
cfgBytes, err := ic.DAO.GetItemCtx().Serialize(cfgItem, false)
if err != nil {
return err
}
ic.DAO.PutStorageItem(p.ID, []byte{palamPrefixConfig}, cfgBytes)
return nil
}
// InitializeCache initializes the contract cache.
func (p *Palam) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
return nil // No cache needed - use storage directly
}
// ActiveIn returns the hardfork at which the contract becomes active.
func (p *Palam) ActiveIn() *config.Hardfork {
return nil // Always active
}
// Address returns the contract's script hash.
func (p *Palam) Address() util.Uint160 {
return p.Hash
}
// GetFlowInternal returns a flow by ID (for cross-contract use).
func (p *Palam) GetFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow {
return p.getFlowInternal(d, flowID)
}
// HasDeclassifyGrant checks if requester has declassify grant for a flow (for cross-contract use).
func (p *Palam) HasDeclassifyGrant(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool {
return p.hasGrantInternal(d, flowID, requester)
}
// OnPersist implements the Contract interface.
func (p *Palam) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements the Contract interface.
func (p *Palam) PostPersist(ic *interop.Context) error {
return nil
}
// ===== Configuration =====
func (p *Palam) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
cfg := p.getConfigInternal(ic.DAO)
return cfg.ToStackItem()
}
func (p *Palam) getConfigInternal(d *dao.Simple) *state.PalamConfig {
si := d.GetStorageItem(p.ID, []byte{palamPrefixConfig})
if si == nil {
return state.DefaultPalamConfig()
}
item, err := stackitem.Deserialize(si)
if err != nil {
return state.DefaultPalamConfig()
}
cfg := &state.PalamConfig{}
if err := cfg.FromStackItem(item); err != nil {
return state.DefaultPalamConfig()
}
return cfg
}
// ===== Counter Helpers =====
func (p *Palam) getFlowCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(p.ID, []byte{palamPrefixFlowCounter})
if si == nil || len(si) == 0 {
return 0
}
return binary.LittleEndian.Uint64(si)
}
func (p *Palam) setFlowCounter(d *dao.Simple, count uint64) {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, count)
d.PutStorageItem(p.ID, []byte{palamPrefixFlowCounter}, data)
}
func (p *Palam) getRequestCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(p.ID, []byte{palamPrefixRequestCounter})
if si == nil || len(si) == 0 {
return 0
}
return binary.LittleEndian.Uint64(si)
}
func (p *Palam) setRequestCounter(d *dao.Simple, count uint64) {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, count)
d.PutStorageItem(p.ID, []byte{palamPrefixRequestCounter}, data)
}
func (p *Palam) getNextRequestID(d *dao.Simple) uint64 {
id := p.getRequestCounter(d) + 1
p.setRequestCounter(d, id)
return id
}
func (p *Palam) getLogCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(p.ID, []byte{palamPrefixLogCounter})
if si == nil || len(si) == 0 {
return 0
}
return binary.LittleEndian.Uint64(si)
}
func (p *Palam) setLogCounter(d *dao.Simple, count uint64) {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, count)
d.PutStorageItem(p.ID, []byte{palamPrefixLogCounter}, data)
}
func (p *Palam) getNextLogID(d *dao.Simple) uint64 {
id := p.getLogCounter(d) + 1
p.setLogCounter(d, id)
return id
}
func (p *Palam) getAttachmentCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(p.ID, []byte{palamPrefixAttachmentCounter})
if si == nil || len(si) == 0 {
return 0
}
return binary.LittleEndian.Uint64(si)
}
func (p *Palam) setAttachmentCounter(d *dao.Simple, count uint64) {
data := make([]byte, 8)
binary.LittleEndian.PutUint64(data, count)
d.PutStorageItem(p.ID, []byte{palamPrefixAttachmentCounter}, data)
}
func (p *Palam) getNextAttachmentID(d *dao.Simple) uint64 {
id := p.getAttachmentCounter(d) + 1
p.setAttachmentCounter(d, id)
return id
}
// ===== Flow Recording =====
func (p *Palam) recordFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
tag, err := stackitem.ToString(args[0])
if err != nil || len(tag) == 0 || len(tag) > 64 {
panic(ErrPalamInvalidTag)
}
amountBI, err := args[1].TryInteger()
if err != nil {
panic(err)
}
amount := amountBI.Uint64()
bucket, err := stackitem.ToString(args[2])
if err != nil || len(bucket) == 0 || len(bucket) > 32 {
panic(ErrPalamInvalidBucket)
}
participantsArr, ok := args[3].Value().([]stackitem.Item)
if !ok {
panic("invalid participants array")
}
participants := make([]util.Uint160, len(participantsArr))
for i, p := range participantsArr {
pBytes, err := p.TryBytes()
if err != nil {
panic(err)
}
participants[i], err = util.Uint160DecodeBytesBE(pBytes)
if err != nil {
panic(err)
}
}
consumerData, err := args[4].TryBytes()
if err != nil {
panic(err)
}
merchantData, err := args[5].TryBytes()
if err != nil {
panic(err)
}
distributorData, err := args[6].TryBytes()
if err != nil {
panic(err)
}
producerData, err := args[7].TryBytes()
if err != nil {
panic(err)
}
ngoData, err := args[8].TryBytes()
if err != nil {
panic(err)
}
auditorData, err := args[9].TryBytes()
if err != nil {
panic(err)
}
prevFlowBytes, err := args[10].TryBytes()
if err != nil {
panic(err)
}
var previousFlowID util.Uint256
if len(prevFlowBytes) == 32 {
previousFlowID, err = util.Uint256DecodeBytesBE(prevFlowBytes)
if err != nil {
panic(err)
}
}
caller := ic.VM.GetCallingScriptHash()
// Verify caller has Vita
if p.Vita != nil && !p.Vita.TokenExists(ic.DAO, caller) {
panic(ErrPalamNoVita)
}
// Generate flow ID from content hash
flowData := append([]byte(tag), []byte(bucket)...)
flowData = append(flowData, caller.BytesBE()...)
flowData = append(flowData, consumerData...)
flowData = append(flowData, merchantData...)
blockBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(blockBytes, ic.Block.Index)
flowData = append(flowData, blockBytes...)
flowID := hash.Sha256(flowData)
// Check if flow already exists
if p.getFlowInternal(ic.DAO, flowID) != nil {
panic(ErrPalamFlowExists)
}
flow := &state.Flow{
FlowID: flowID,
Bucket: bucket,
Tag: tag,
Amount: amount,
Timestamp: ic.Block.Index,
Creator: caller,
ConsumerData: consumerData,
MerchantData: merchantData,
DistributorData: distributorData,
ProducerData: producerData,
NGOData: ngoData,
AuditorData: auditorData,
Participants: participants,
PreviousFlowID: previousFlowID,
Status: state.FlowStatusActive,
}
// Store flow
p.putFlow(ic.DAO, flow)
// Update counter
count := p.getFlowCounter(ic.DAO) + 1
p.setFlowCounter(ic.DAO, count)
// Emit event
ic.AddNotification(p.Hash, FlowRecordedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(flowID.BytesBE()),
stackitem.NewByteArray([]byte(tag)),
stackitem.NewByteArray([]byte(bucket)),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ic.Block.Index))),
}))
return stackitem.NewByteArray(flowID.BytesBE())
}
func (p *Palam) putFlow(d *dao.Simple, flow *state.Flow) {
flowItem := flow.ToStackItem()
flowBytes, err := d.GetItemCtx().Serialize(flowItem, false)
if err != nil {
panic(err)
}
// Store main record
key := append([]byte{palamPrefixFlow}, flow.FlowID.BytesBE()...)
d.PutStorageItem(p.ID, key, flowBytes)
// Index by bucket
bucketKey := append([]byte{palamPrefixFlowByBucket}, []byte(flow.Bucket)...)
bucketKey = append(bucketKey, flow.FlowID.BytesBE()...)
d.PutStorageItem(p.ID, bucketKey, []byte{1})
// Index by tag
tagKey := append([]byte{palamPrefixFlowByTag}, []byte(flow.Tag)...)
tagKey = append(tagKey, flow.FlowID.BytesBE()...)
d.PutStorageItem(p.ID, tagKey, []byte{1})
// Index by participants
for _, participant := range flow.Participants {
partKey := append([]byte{palamPrefixFlowByParticipant}, participant.BytesBE()...)
partKey = append(partKey, flow.FlowID.BytesBE()...)
d.PutStorageItem(p.ID, partKey, []byte{1})
}
}
func (p *Palam) getFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow {
key := append([]byte{palamPrefixFlow}, flowID.BytesBE()...)
si := d.GetStorageItem(p.ID, key)
if si == nil {
return nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil
}
flow := &state.Flow{}
if err := flow.FromStackItem(item); err != nil {
return nil
}
return flow
}
func (p *Palam) getFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
flowIDBytes, err := args[0].TryBytes()
if err != nil {
panic(err)
}
flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
if err != nil {
panic(err)
}
flow := p.getFlowInternal(ic.DAO, flowID)
if flow == nil {
return stackitem.Null{}
}
// Log access if configured
cfg := p.getConfigInternal(ic.DAO)
if cfg.LogAllAccess {
caller := ic.VM.GetCallingScriptHash()
p.logAccess(ic, flowID, caller, state.AccessTypeView, "getFlow")
}
return flow.ToStackItem()
}
func (p *Palam) getTotalFlows(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getFlowCounter(ic.DAO)))
}
// ===== Attachments =====
func (p *Palam) attachToFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
flowIDBytes, err := args[0].TryBytes()
if err != nil {
panic(err)
}
flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
if err != nil {
panic(err)
}
attachmentType, err := stackitem.ToString(args[1])
if err != nil || len(attachmentType) == 0 || len(attachmentType) > 64 {
panic(ErrPalamInvalidAttachment)
}
encryptedData, err := args[2].TryBytes()
if err != nil {
panic(err)
}
caller := ic.VM.GetCallingScriptHash()
// Verify flow exists
flow := p.getFlowInternal(ic.DAO, flowID)
if flow == nil {
panic(ErrPalamFlowNotFound)
}
// Verify caller is a participant
isParticipant := false
for _, p := range flow.Participants {
if p.Equals(caller) {
isParticipant = true
break
}
}
if !isParticipant && !flow.Creator.Equals(caller) {
panic(ErrPalamNotParticipant)
}
attachmentID := p.getNextAttachmentID(ic.DAO)
attachment := &state.FlowAttachment{
AttachmentID: attachmentID,
FlowID: flowID,
AttachmentType: attachmentType,
EncryptedData: encryptedData,
Attacher: caller,
AttachedAt: ic.Block.Index,
}
p.putAttachment(ic.DAO, attachment)
// Emit event
ic.AddNotification(p.Hash, FlowAttachmentEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(flowID.BytesBE()),
stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID)),
stackitem.NewByteArray([]byte(attachmentType)),
}))
return stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID))
}
func (p *Palam) putAttachment(d *dao.Simple, att *state.FlowAttachment) {
attItem := att.ToStackItem()
attBytes, err := d.GetItemCtx().Serialize(attItem, false)
if err != nil {
panic(err)
}
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, att.AttachmentID)
// Store main record
key := append([]byte{palamPrefixAttachment}, idBytes...)
d.PutStorageItem(p.ID, key, attBytes)
// Index by flow
flowKey := append([]byte{palamPrefixAttachmentByFlow}, att.FlowID.BytesBE()...)
flowKey = append(flowKey, idBytes...)
d.PutStorageItem(p.ID, flowKey, []byte{1})
}
func (p *Palam) getAttachmentInternal(d *dao.Simple, attachmentID uint64) *state.FlowAttachment {
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, attachmentID)
key := append([]byte{palamPrefixAttachment}, idBytes...)
si := d.GetStorageItem(p.ID, key)
if si == nil {
return nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil
}
att := &state.FlowAttachment{}
if err := att.FromStackItem(item); err != nil {
return nil
}
return att
}
func (p *Palam) getAttachment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
attachmentID, err := args[0].TryInteger()
if err != nil {
panic(err)
}
att := p.getAttachmentInternal(ic.DAO, attachmentID.Uint64())
if att == nil {
return stackitem.Null{}
}
return att.ToStackItem()
}
func (p *Palam) getTotalAttachments(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getAttachmentCounter(ic.DAO)))
}
// ===== Declassification =====
func (p *Palam) requestDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
flowIDBytes, err := args[0].TryBytes()
if err != nil {
panic(err)
}
flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
if err != nil {
panic(err)
}
caseID, err := stackitem.ToString(args[1])
if err != nil || len(caseID) == 0 || len(caseID) > 64 {
panic(ErrPalamInvalidCaseID)
}
reason, err := stackitem.ToString(args[2])
if err != nil || len(reason) < 50 || len(reason) > 1024 {
panic(ErrPalamInvalidReason)
}
expirationBlocks, err := args[3].TryInteger()
if err != nil {
panic(err)
}
expBlocks := uint32(expirationBlocks.Uint64())
caller := ic.VM.GetCallingScriptHash()
// Verify flow exists
flow := p.getFlowInternal(ic.DAO, flowID)
if flow == nil {
panic(ErrPalamFlowNotFound)
}
// Verify caller is an auditor
if p.RoleRegistry != nil {
if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamAuditor, ic.Block.Index) {
panic(ErrPalamNotAuditor)
}
}
// Get config for defaults
cfg := p.getConfigInternal(ic.DAO)
if expBlocks == 0 {
expBlocks = cfg.DefaultExpiration
}
requestID := p.getNextRequestID(ic.DAO)
request := &state.DeclassifyRequest{
RequestID: requestID,
FlowID: flowID,
CaseID: caseID,
Reason: reason,
Requester: caller,
RequesterRole: state.PalamRoleAuditor,
RequiredApprovals: cfg.MinApprovals,
Approvals: []util.Uint160{},
ApprovalTimes: []uint32{},
Status: state.DeclassifyPending,
CreatedAt: ic.Block.Index,
ExpiresAt: ic.Block.Index + expBlocks,
GrantedAt: 0,
}
p.putDeclassifyRequest(ic.DAO, request)
// Emit event
ic.AddNotification(p.Hash, DeclassifyRequestedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)),
stackitem.NewByteArray(flowID.BytesBE()),
stackitem.NewByteArray([]byte(caseID)),
stackitem.NewByteArray(caller.BytesBE()),
}))
return stackitem.NewBigInteger(new(big.Int).SetUint64(requestID))
}
func (p *Palam) putDeclassifyRequest(d *dao.Simple, req *state.DeclassifyRequest) {
reqItem := req.ToStackItem()
reqBytes, err := d.GetItemCtx().Serialize(reqItem, false)
if err != nil {
panic(err)
}
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, req.RequestID)
// Store main record
key := append([]byte{palamPrefixRequest}, idBytes...)
d.PutStorageItem(p.ID, key, reqBytes)
// Index by flow
flowKey := append([]byte{palamPrefixRequestByFlow}, req.FlowID.BytesBE()...)
flowKey = append(flowKey, idBytes...)
d.PutStorageItem(p.ID, flowKey, []byte{1})
// Index by requester
reqKey := append([]byte{palamPrefixRequestByRequester}, req.Requester.BytesBE()...)
reqKey = append(reqKey, idBytes...)
d.PutStorageItem(p.ID, reqKey, []byte{1})
// Track pending request for flow
if req.Status == state.DeclassifyPending {
pendingKey := append([]byte{palamPrefixPendingRequest}, req.FlowID.BytesBE()...)
d.PutStorageItem(p.ID, pendingKey, idBytes)
}
}
func (p *Palam) getRequestInternal(d *dao.Simple, requestID uint64) *state.DeclassifyRequest {
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, requestID)
key := append([]byte{palamPrefixRequest}, idBytes...)
si := d.GetStorageItem(p.ID, key)
if si == nil {
return nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil
}
req := &state.DeclassifyRequest{}
if err := req.FromStackItem(item); err != nil {
return nil
}
return req
}
func (p *Palam) approveDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestID, err := args[0].TryInteger()
if err != nil {
panic(err)
}
caller := ic.VM.GetCallingScriptHash()
// Verify caller is a judge
if p.RoleRegistry != nil {
if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) {
panic(ErrPalamNotJudge)
}
}
req := p.getRequestInternal(ic.DAO, requestID.Uint64())
if req == nil {
panic(ErrPalamRequestNotFound)
}
// Check request is pending
if req.Status != state.DeclassifyPending {
panic(ErrPalamRequestNotPending)
}
// Check not expired
if ic.Block.Index > req.ExpiresAt {
req.Status = state.DeclassifyExpired
p.putDeclassifyRequest(ic.DAO, req)
ic.AddNotification(p.Hash, DeclassifyExpiredEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
}))
panic(ErrPalamRequestExpired)
}
// Cannot approve own request
if caller.Equals(req.Requester) {
panic(ErrPalamCannotSelfApprove)
}
// Check not already approved by this caller
for _, approver := range req.Approvals {
if approver.Equals(caller) {
panic(ErrPalamAlreadyApproved)
}
}
// Add approval
req.Approvals = append(req.Approvals, caller)
req.ApprovalTimes = append(req.ApprovalTimes, ic.Block.Index)
// Check if enough approvals
if uint32(len(req.Approvals)) >= req.RequiredApprovals {
req.Status = state.DeclassifyApproved
req.GrantedAt = ic.Block.Index
// Emit granted event
ic.AddNotification(p.Hash, DeclassifyGrantedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
stackitem.NewByteArray(req.FlowID.BytesBE()),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.GrantedAt))),
}))
}
p.putDeclassifyRequest(ic.DAO, req)
// Emit approval event
ic.AddNotification(p.Hash, DeclassifyApprovalEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
stackitem.NewByteArray(caller.BytesBE()),
stackitem.NewBigInteger(new(big.Int).SetInt64(int64(len(req.Approvals)))),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.RequiredApprovals))),
}))
return stackitem.NewBool(true)
}
func (p *Palam) denyDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestID, err := args[0].TryInteger()
if err != nil {
panic(err)
}
reason, err := stackitem.ToString(args[1])
if err != nil {
panic(err)
}
caller := ic.VM.GetCallingScriptHash()
// Verify caller is a judge
if p.RoleRegistry != nil {
if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) {
panic(ErrPalamNotJudge)
}
}
req := p.getRequestInternal(ic.DAO, requestID.Uint64())
if req == nil {
panic(ErrPalamRequestNotFound)
}
if req.Status != state.DeclassifyPending {
panic(ErrPalamRequestNotPending)
}
req.Status = state.DeclassifyDenied
p.putDeclassifyRequest(ic.DAO, req)
// Emit denied event
ic.AddNotification(p.Hash, DeclassifyDeniedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
stackitem.NewByteArray(caller.BytesBE()),
stackitem.NewByteArray([]byte(reason)),
}))
return stackitem.NewBool(true)
}
func (p *Palam) getDeclassifyRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
requestID, err := args[0].TryInteger()
if err != nil {
panic(err)
}
req := p.getRequestInternal(ic.DAO, requestID.Uint64())
if req == nil {
return stackitem.Null{}
}
return req.ToStackItem()
}
func (p *Palam) getTotalRequests(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getRequestCounter(ic.DAO)))
}
func (p *Palam) hasDeclassifyGrant(ic *interop.Context, args []stackitem.Item) stackitem.Item {
flowIDBytes, err := args[0].TryBytes()
if err != nil {
panic(err)
}
flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
if err != nil {
panic(err)
}
requesterBytes, err := args[1].TryBytes()
if err != nil {
panic(err)
}
requester, err := util.Uint160DecodeBytesBE(requesterBytes)
if err != nil {
panic(err)
}
// Search for approved request by this requester for this flow
return stackitem.NewBool(p.hasGrantInternal(ic.DAO, flowID, requester))
}
func (p *Palam) hasGrantInternal(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool {
// Iterate through requests for this flow
prefix := append([]byte{palamPrefixRequestByFlow}, flowID.BytesBE()...)
d.Seek(p.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 8 {
return true
}
requestID := binary.LittleEndian.Uint64(k[len(k)-8:])
req := p.getRequestInternal(d, requestID)
if req != nil && req.Status == state.DeclassifyApproved && req.Requester.Equals(requester) {
return false // Found a grant
}
return true
})
// Note: This is a simplification - in production would track this more efficiently
return false
}
// ===== Access Logging =====
func (p *Palam) logAccess(ic *interop.Context, flowID util.Uint256, accessor util.Uint160, accessType state.AccessType, details string) {
logID := p.getNextLogID(ic.DAO)
log := &state.AccessLog{
LogID: logID,
FlowID: flowID,
Accessor: accessor,
AccessType: accessType,
Timestamp: ic.Block.Index,
Details: details,
}
p.putAccessLog(ic.DAO, log)
// Emit event
ic.AddNotification(p.Hash, FlowAccessedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(flowID.BytesBE()),
stackitem.NewByteArray(accessor.BytesBE()),
stackitem.NewByteArray([]byte(details)),
}))
}
func (p *Palam) putAccessLog(d *dao.Simple, log *state.AccessLog) {
logItem := log.ToStackItem()
logBytes, err := d.GetItemCtx().Serialize(logItem, false)
if err != nil {
panic(err)
}
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, log.LogID)
// Store main record
key := append([]byte{palamPrefixAccessLog}, idBytes...)
d.PutStorageItem(p.ID, key, logBytes)
// Index by flow
flowKey := append([]byte{palamPrefixLogByFlow}, log.FlowID.BytesBE()...)
flowKey = append(flowKey, idBytes...)
d.PutStorageItem(p.ID, flowKey, []byte{1})
// Index by accessor
accKey := append([]byte{palamPrefixLogByAccessor}, log.Accessor.BytesBE()...)
accKey = append(accKey, idBytes...)
d.PutStorageItem(p.ID, accKey, []byte{1})
}
func (p *Palam) getAccessLogInternal(d *dao.Simple, logID uint64) *state.AccessLog {
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, logID)
key := append([]byte{palamPrefixAccessLog}, idBytes...)
si := d.GetStorageItem(p.ID, key)
if si == nil {
return nil
}
item, err := stackitem.Deserialize(si)
if err != nil {
return nil
}
log := &state.AccessLog{}
if err := log.FromStackItem(item); err != nil {
return nil
}
return log
}
func (p *Palam) getAccessLog(ic *interop.Context, args []stackitem.Item) stackitem.Item {
logID, err := args[0].TryInteger()
if err != nil {
panic(err)
}
log := p.getAccessLogInternal(ic.DAO, logID.Uint64())
if log == nil {
return stackitem.Null{}
}
return log.ToStackItem()
}
func (p *Palam) getTotalLogs(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getLogCounter(ic.DAO)))
}
// ===== Role Permissions =====
func (p *Palam) getRolePermissions(ic *interop.Context, args []stackitem.Item) stackitem.Item {
roleInt, err := args[0].TryInteger()
if err != nil {
panic(err)
}
role := state.PalamRole(roleInt.Uint64())
permissions := state.DefaultRolePermissions(role)
return permissions.ToStackItem()
}