420 lines
12 KiB
Go
420 lines
12 KiB
Go
package state
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
)
|
|
|
|
// ProposalCategory represents the type of proposal.
|
|
type ProposalCategory uint8
|
|
|
|
// Proposal categories.
|
|
const (
|
|
ProposalCategoryLawAmendment ProposalCategory = 1 // Lex integration
|
|
ProposalCategoryInvestment ProposalCategory = 2 // PIO/EIO/CIO (future)
|
|
ProposalCategoryGovernanceAction ProposalCategory = 3 // Committee/policy changes
|
|
ProposalCategoryConstitutional ProposalCategory = 4 // Supermajority required
|
|
ProposalCategoryReferendum ProposalCategory = 5 // Citizen-initiated
|
|
)
|
|
|
|
// ProposalStatus represents the lifecycle status of a proposal.
|
|
type ProposalStatus uint8
|
|
|
|
// Proposal statuses.
|
|
const (
|
|
ProposalStatusDraft ProposalStatus = 0 // Not yet open for voting
|
|
ProposalStatusActive ProposalStatus = 1 // Open for voting
|
|
ProposalStatusPassed ProposalStatus = 2 // Met threshold, awaiting execution
|
|
ProposalStatusRejected ProposalStatus = 3 // Failed to meet threshold
|
|
ProposalStatusExecuted ProposalStatus = 4 // Successfully executed
|
|
ProposalStatusCancelled ProposalStatus = 5 // Cancelled by proposer/committee
|
|
ProposalStatusExpired ProposalStatus = 6 // Voting period ended without quorum
|
|
)
|
|
|
|
// VoteChoice represents a voting decision.
|
|
type VoteChoice uint8
|
|
|
|
// Vote choices.
|
|
const (
|
|
VoteChoiceAbstain VoteChoice = 0
|
|
VoteChoiceYes VoteChoice = 1
|
|
VoteChoiceNo VoteChoice = 2
|
|
)
|
|
|
|
// Proposal represents a democratic proposal in the Eligere voting system.
|
|
type Proposal struct {
|
|
ID uint64 // Unique proposal ID
|
|
Title string // Short title (max 128 chars)
|
|
ContentHash util.Uint256 // Hash of full proposal content (stored off-chain)
|
|
Category ProposalCategory // Type of proposal
|
|
Proposer util.Uint160 // Who created the proposal
|
|
ProposerVitaID uint64 // Vita ID of proposer
|
|
|
|
// Timing (block heights)
|
|
CreatedAt uint32 // When created
|
|
VotingStartsAt uint32 // When voting opens
|
|
VotingEndsAt uint32 // elegireDeadline - when voting closes
|
|
ExecutionDelay uint32 // Blocks after passing before execution allowed
|
|
|
|
// Thresholds
|
|
QuorumPercent uint8 // Minimum participation percentage (e.g., 10%)
|
|
ThresholdPercent uint8 // Required yes votes percentage (50% or 67%)
|
|
|
|
// Results
|
|
Status ProposalStatus
|
|
TotalVotes uint64 // Total votes cast
|
|
YesVotes uint64 // Votes in favor
|
|
NoVotes uint64 // Votes against
|
|
AbstainVotes uint64 // Abstentions (count toward quorum)
|
|
|
|
// Execution target (for contract integrations like Lex)
|
|
TargetContract util.Uint160 // Contract to call on execution
|
|
TargetMethod string // Method to call
|
|
TargetParams []byte // Serialized parameters
|
|
|
|
// Execution tracking
|
|
ExecutedAt uint32 // When executed (0 if not)
|
|
ExecutedBy util.Uint160 // Who executed
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (p *Proposal) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.ID))),
|
|
stackitem.NewByteArray([]byte(p.Title)),
|
|
stackitem.NewByteArray(p.ContentHash[:]),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.Category))),
|
|
stackitem.NewByteArray(p.Proposer.BytesBE()),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.ProposerVitaID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.CreatedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.VotingStartsAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.VotingEndsAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.ExecutionDelay))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.QuorumPercent))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.ThresholdPercent))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.Status))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.TotalVotes))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.YesVotes))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.NoVotes))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.AbstainVotes))),
|
|
stackitem.NewByteArray(p.TargetContract.BytesBE()),
|
|
stackitem.NewByteArray([]byte(p.TargetMethod)),
|
|
stackitem.NewByteArray(p.TargetParams),
|
|
stackitem.NewBigInteger(big.NewInt(int64(p.ExecutedAt))),
|
|
stackitem.NewByteArray(p.ExecutedBy.BytesBE()),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (p *Proposal) FromStackItem(item stackitem.Item) error {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(arr) < 22 {
|
|
return errors.New("invalid proposal struct length")
|
|
}
|
|
|
|
id, err := arr[0].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ID = id.Uint64()
|
|
|
|
titleBytes, err := arr[1].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Title = string(titleBytes)
|
|
|
|
contentHashBytes, err := arr[2].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(contentHashBytes) == 32 {
|
|
copy(p.ContentHash[:], contentHashBytes)
|
|
}
|
|
|
|
category, err := arr[3].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Category = ProposalCategory(category.Uint64())
|
|
|
|
proposerBytes, err := arr[4].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Proposer, _ = util.Uint160DecodeBytesBE(proposerBytes)
|
|
|
|
proposerVitaID, err := arr[5].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ProposerVitaID = proposerVitaID.Uint64()
|
|
|
|
createdAt, err := arr[6].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.CreatedAt = uint32(createdAt.Uint64())
|
|
|
|
votingStartsAt, err := arr[7].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.VotingStartsAt = uint32(votingStartsAt.Uint64())
|
|
|
|
votingEndsAt, err := arr[8].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.VotingEndsAt = uint32(votingEndsAt.Uint64())
|
|
|
|
executionDelay, err := arr[9].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ExecutionDelay = uint32(executionDelay.Uint64())
|
|
|
|
quorumPercent, err := arr[10].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.QuorumPercent = uint8(quorumPercent.Uint64())
|
|
|
|
thresholdPercent, err := arr[11].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ThresholdPercent = uint8(thresholdPercent.Uint64())
|
|
|
|
status, err := arr[12].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Status = ProposalStatus(status.Uint64())
|
|
|
|
totalVotes, err := arr[13].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.TotalVotes = totalVotes.Uint64()
|
|
|
|
yesVotes, err := arr[14].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.YesVotes = yesVotes.Uint64()
|
|
|
|
noVotes, err := arr[15].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.NoVotes = noVotes.Uint64()
|
|
|
|
abstainVotes, err := arr[16].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.AbstainVotes = abstainVotes.Uint64()
|
|
|
|
targetContractBytes, err := arr[17].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.TargetContract, _ = util.Uint160DecodeBytesBE(targetContractBytes)
|
|
|
|
targetMethodBytes, err := arr[18].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.TargetMethod = string(targetMethodBytes)
|
|
|
|
targetParams, err := arr[19].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.TargetParams = targetParams
|
|
|
|
executedAt, err := arr[20].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ExecutedAt = uint32(executedAt.Uint64())
|
|
|
|
executedByBytes, err := arr[21].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ExecutedBy, _ = util.Uint160DecodeBytesBE(executedByBytes)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Vote represents a single vote record in the Eligere voting system.
|
|
type Vote struct {
|
|
ProposalID uint64 // Which proposal
|
|
VoterVitaID uint64 // Vita ID of voter (ensures one-person-one-vote)
|
|
Voter util.Uint160 // Voter's address
|
|
Choice VoteChoice // How they voted
|
|
VotedAt uint32 // Block height when voted
|
|
Weight uint64 // Vote weight (1 for equal voting)
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (v *Vote) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(v.ProposalID))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(v.VoterVitaID))),
|
|
stackitem.NewByteArray(v.Voter.BytesBE()),
|
|
stackitem.NewBigInteger(big.NewInt(int64(v.Choice))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(v.VotedAt))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(v.Weight))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (v *Vote) FromStackItem(item stackitem.Item) error {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(arr) < 6 {
|
|
return errors.New("invalid vote struct length")
|
|
}
|
|
|
|
proposalID, err := arr[0].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.ProposalID = proposalID.Uint64()
|
|
|
|
voterVitaID, err := arr[1].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.VoterVitaID = voterVitaID.Uint64()
|
|
|
|
voterBytes, err := arr[2].TryBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Voter, _ = util.Uint160DecodeBytesBE(voterBytes)
|
|
|
|
choice, err := arr[3].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Choice = VoteChoice(choice.Uint64())
|
|
|
|
votedAt, err := arr[4].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.VotedAt = uint32(votedAt.Uint64())
|
|
|
|
weight, err := arr[5].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Weight = weight.Uint64()
|
|
|
|
return nil
|
|
}
|
|
|
|
// EligereConfig represents configurable parameters for the Eligere voting system.
|
|
type EligereConfig struct {
|
|
DefaultVotingPeriod uint32 // Default blocks for voting (~7 days at 1s blocks = 604800)
|
|
MinVotingPeriod uint32 // Minimum blocks (~1 day = 86400)
|
|
MaxVotingPeriod uint32 // Maximum blocks (~30 days = 2592000)
|
|
DefaultExecutionDelay uint32 // Blocks between passing and execution (~1 day = 86400)
|
|
DefaultQuorum uint8 // Default quorum percentage (e.g., 10%)
|
|
ConstitutionalThreshold uint8 // Supermajority threshold (e.g., 67%)
|
|
StandardThreshold uint8 // Simple majority (e.g., 51%)
|
|
}
|
|
|
|
// ToStackItem implements stackitem.Convertible interface.
|
|
func (c *EligereConfig) ToStackItem() (stackitem.Item, error) {
|
|
return stackitem.NewStruct([]stackitem.Item{
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultVotingPeriod))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.MinVotingPeriod))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.MaxVotingPeriod))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultExecutionDelay))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultQuorum))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.ConstitutionalThreshold))),
|
|
stackitem.NewBigInteger(big.NewInt(int64(c.StandardThreshold))),
|
|
}), nil
|
|
}
|
|
|
|
// FromStackItem implements stackitem.Convertible interface.
|
|
func (c *EligereConfig) FromStackItem(item stackitem.Item) error {
|
|
arr, ok := item.Value().([]stackitem.Item)
|
|
if !ok {
|
|
return errors.New("not a struct")
|
|
}
|
|
if len(arr) < 7 {
|
|
return errors.New("invalid config struct length")
|
|
}
|
|
|
|
defaultVotingPeriod, err := arr[0].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.DefaultVotingPeriod = uint32(defaultVotingPeriod.Uint64())
|
|
|
|
minVotingPeriod, err := arr[1].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.MinVotingPeriod = uint32(minVotingPeriod.Uint64())
|
|
|
|
maxVotingPeriod, err := arr[2].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.MaxVotingPeriod = uint32(maxVotingPeriod.Uint64())
|
|
|
|
defaultExecutionDelay, err := arr[3].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.DefaultExecutionDelay = uint32(defaultExecutionDelay.Uint64())
|
|
|
|
defaultQuorum, err := arr[4].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.DefaultQuorum = uint8(defaultQuorum.Uint64())
|
|
|
|
constitutionalThreshold, err := arr[5].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.ConstitutionalThreshold = uint8(constitutionalThreshold.Uint64())
|
|
|
|
standardThreshold, err := arr[6].TryInteger()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.StandardThreshold = uint8(standardThreshold.Uint64())
|
|
|
|
return nil
|
|
}
|
|
|
|
// DefaultEligereConfig returns the default configuration for Eligere.
|
|
func DefaultEligereConfig() *EligereConfig {
|
|
return &EligereConfig{
|
|
DefaultVotingPeriod: 604800, // ~7 days at 1 block/second
|
|
MinVotingPeriod: 86400, // ~1 day minimum
|
|
MaxVotingPeriod: 2592000, // ~30 days maximum
|
|
DefaultExecutionDelay: 86400, // ~1 day delay after passing
|
|
DefaultQuorum: 10, // 10% participation required
|
|
ConstitutionalThreshold: 67, // 67% supermajority for constitutional
|
|
StandardThreshold: 51, // 51% simple majority
|
|
}
|
|
}
|