diff --git a/consensus/state.go b/consensus/state.go index 8cd0bfe..93d60c9 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -97,13 +97,13 @@ func (n *Network) GenesisState() State { ChildTarget: n.InitialTarget, SiafundPool: types.ZeroCurrency, - OakTime: 0, - OakTarget: intToTarget(maxTarget), - FoundationPrimaryAddress: n.HardforkFoundation.PrimaryAddress, - FoundationFailsafeAddress: n.HardforkFoundation.FailsafeAddress, - TotalWork: Work{invTarget(intToTarget(maxTarget))}, - Difficulty: Work{invTarget(n.InitialTarget)}, - OakWork: Work{invTarget(intToTarget(maxTarget))}, + OakTime: 0, + OakTarget: intToTarget(maxTarget), + FoundationSubsidyAddress: n.HardforkFoundation.PrimaryAddress, + FoundationManagementAddress: n.HardforkFoundation.FailsafeAddress, + TotalWork: Work{invTarget(intToTarget(maxTarget))}, + Difficulty: Work{invTarget(n.InitialTarget)}, + OakWork: Work{invTarget(intToTarget(maxTarget))}, } } @@ -121,8 +121,8 @@ type State struct { OakTime time.Duration `json:"oakTime"` OakTarget types.BlockID `json:"oakTarget"` // Foundation hardfork state - FoundationPrimaryAddress types.Address `json:"foundationPrimaryAddress"` - FoundationFailsafeAddress types.Address `json:"foundationFailsafeAddress"` + FoundationSubsidyAddress types.Address `json:"foundationSubsidyAddress"` + FoundationManagementAddress types.Address `json:"foundationManagementAddress"` // v2 hardfork state TotalWork Work `json:"totalWork"` Difficulty Work `json:"difficulty"` @@ -143,8 +143,8 @@ func (s State) EncodeTo(e *types.Encoder) { e.WriteUint64(uint64(s.OakTime)) s.OakTarget.EncodeTo(e) - s.FoundationPrimaryAddress.EncodeTo(e) - s.FoundationFailsafeAddress.EncodeTo(e) + s.FoundationSubsidyAddress.EncodeTo(e) + s.FoundationManagementAddress.EncodeTo(e) s.TotalWork.EncodeTo(e) s.Difficulty.EncodeTo(e) s.OakWork.EncodeTo(e) @@ -164,8 +164,8 @@ func (s *State) DecodeFrom(d *types.Decoder) { s.OakTime = time.Duration(d.ReadUint64()) s.OakTarget.DecodeFrom(d) - s.FoundationPrimaryAddress.DecodeFrom(d) - s.FoundationFailsafeAddress.DecodeFrom(d) + s.FoundationSubsidyAddress.DecodeFrom(d) + s.FoundationManagementAddress.DecodeFrom(d) s.TotalWork.DecodeFrom(d) s.Difficulty.DecodeFrom(d) s.OakWork.DecodeFrom(d) @@ -257,22 +257,23 @@ func (s State) AncestorDepth() uint64 { } // FoundationSubsidy returns the Foundation subsidy output for the child block. -// If no subsidy is due, the returned output has a value of zero. func (s State) FoundationSubsidy() (sco types.SiacoinOutput, exists bool) { - sco.Address = s.FoundationPrimaryAddress - + if s.FoundationSubsidyAddress == types.VoidAddress { + return types.SiacoinOutput{}, false + } + sco.Address = s.FoundationSubsidyAddress subsidyPerBlock := types.Siacoins(30000) blocksPerYear := uint64(365 * 24 * time.Hour / s.BlockInterval()) blocksPerMonth := blocksPerYear / 12 hardforkHeight := s.Network.HardforkFoundation.Height if s.childHeight() < hardforkHeight || (s.childHeight()-hardforkHeight)%blocksPerMonth != 0 { - sco.Value = types.ZeroCurrency + return types.SiacoinOutput{}, false } else if s.childHeight() == hardforkHeight { sco.Value = subsidyPerBlock.Mul64(blocksPerYear) } else { sco.Value = subsidyPerBlock.Mul64(blocksPerMonth) } - return sco, !sco.Value.IsZero() + return sco, true } // NonceFactor is the factor by which all block nonces must be divisible. @@ -342,6 +343,9 @@ func (s State) V2TransactionWeight(txn types.V2Transaction) uint64 { a.EncodeTo(e) } e.Write(txn.ArbitraryData) + if txn.NewFoundationAddress != nil { + txn.NewFoundationAddress.EncodeTo(e) + } e.Flush() return uint64(wc.n) } @@ -587,16 +591,16 @@ func (s State) AttestationSigHash(a types.Attestation) types.Hash256 { // A MidState represents the state of the chain within a block. type MidState struct { - base State - created map[types.ElementID]int // indices into element slices - spends map[types.ElementID]types.TransactionID - revs map[types.FileContractID]*types.FileContractElement - res map[types.FileContractID]bool - v2revs map[types.FileContractID]*types.V2FileContractElement - v2res map[types.FileContractID]types.V2FileContractResolutionType - siafundPool types.Currency - foundationPrimary types.Address - foundationFailsafe types.Address + base State + created map[types.ElementID]int // indices into element slices + spends map[types.ElementID]types.TransactionID + revs map[types.FileContractID]*types.FileContractElement + res map[types.FileContractID]bool + v2revs map[types.FileContractID]*types.V2FileContractElement + v2res map[types.FileContractID]types.V2FileContractResolutionType + siafundPool types.Currency + foundationSubsidy types.Address + foundationManagement types.Address // elements created/updated by block sces []types.SiacoinElement @@ -649,16 +653,16 @@ func (ms *MidState) isCreated(id types.ElementID) bool { // NewMidState constructs a MidState initialized to the provided base state. func NewMidState(s State) *MidState { return &MidState{ - base: s, - created: make(map[types.ElementID]int), - spends: make(map[types.ElementID]types.TransactionID), - revs: make(map[types.FileContractID]*types.FileContractElement), - res: make(map[types.FileContractID]bool), - v2revs: make(map[types.FileContractID]*types.V2FileContractElement), - v2res: make(map[types.FileContractID]types.V2FileContractResolutionType), - siafundPool: s.SiafundPool, - foundationPrimary: s.FoundationPrimaryAddress, - foundationFailsafe: s.FoundationFailsafeAddress, + base: s, + created: make(map[types.ElementID]int), + spends: make(map[types.ElementID]types.TransactionID), + revs: make(map[types.FileContractID]*types.FileContractElement), + res: make(map[types.FileContractID]bool), + v2revs: make(map[types.FileContractID]*types.V2FileContractElement), + v2res: make(map[types.FileContractID]types.V2FileContractResolutionType), + siafundPool: s.SiafundPool, + foundationSubsidy: s.FoundationSubsidyAddress, + foundationManagement: s.FoundationManagementAddress, } } diff --git a/consensus/update.go b/consensus/update.go index 92370f3..1c3e3c6 100644 --- a/consensus/update.go +++ b/consensus/update.go @@ -509,8 +509,8 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp if bytes.HasPrefix(arb, types.SpecifierFoundation[:]) { var update types.FoundationAddressUpdate update.DecodeFrom(types.NewBufDecoder(arb[len(types.SpecifierFoundation):])) - ms.foundationPrimary = update.NewPrimary - ms.foundationFailsafe = update.NewFailsafe + ms.foundationSubsidy = update.NewPrimary + ms.foundationManagement = update.NewFailsafe } } } @@ -566,8 +566,13 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { }) } if txn.NewFoundationAddress != nil { - ms.foundationPrimary = *txn.NewFoundationAddress - ms.foundationFailsafe = *txn.NewFoundationAddress + // The subsidy may be waived by sending it to the void address. In this + // case, the management address is not updated (as this would + // permanently disable the subsidy). + ms.foundationSubsidy = *txn.NewFoundationAddress + if *txn.NewFoundationAddress != types.VoidAddress { + ms.foundationManagement = *txn.NewFoundationAddress + } } } @@ -736,8 +741,8 @@ func ApplyBlock(s State, b types.Block, bs V1BlockSupplement, targetTimestamp ti ms.ApplyBlock(b, bs) s.SiafundPool = ms.siafundPool s.Attestations += uint64(len(ms.aes)) - s.FoundationPrimaryAddress = ms.foundationPrimary - s.FoundationFailsafeAddress = ms.foundationFailsafe + s.FoundationSubsidyAddress = ms.foundationSubsidy + s.FoundationManagementAddress = ms.foundationManagement // compute updated and added elements var updated, added []elementLeaf diff --git a/consensus/update_test.go b/consensus/update_test.go index 6cba2ad..c20280a 100644 --- a/consensus/update_test.go +++ b/consensus/update_test.go @@ -1296,3 +1296,123 @@ func TestApplyRevertBlockV2(t *testing.T) { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } } + +func TestFoundationSubsidy(t *testing.T) { + key := types.GeneratePrivateKey() + addr := types.StandardAddress(key.PublicKey()) + n, genesisBlock := testnet() + n.HardforkFoundation.Height = 1 + n.HardforkFoundation.PrimaryAddress = addr + n.HardforkFoundation.FailsafeAddress = addr + n.HardforkV2.AllowHeight = 1 + n.HardforkV2.RequireHeight = 1 + n.BlockInterval = 10 * time.Hour // subsidies every 10 blocks + subsidyInterval := uint64(365 * 24 * time.Hour / n.BlockInterval / 12) + genesisBlock.Transactions = []types.Transaction{{ + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: types.Siacoins(1), // funds for changing address later + }}, + }} + scoid := genesisBlock.Transactions[0].SiacoinOutputID(0) + + db, cs := newConsensusDB(n, genesisBlock) + mineBlock := func(txns []types.V2Transaction) (subsidy types.SiacoinElement, exists bool) { + b := types.Block{ + ParentID: cs.Index.ID, + Timestamp: types.CurrentTimestamp(), + MinerPayouts: []types.SiacoinOutput{{Address: types.VoidAddress, Value: cs.BlockReward()}}, + V2: &types.V2BlockData{ + Height: cs.Index.Height + 1, + Commitment: cs.Commitment(cs.TransactionsCommitment(nil, txns), types.VoidAddress), + Transactions: txns, + }, + } + bs := db.supplementTipBlock(b) + findBlockNonce(cs, &b) + if err := ValidateBlock(cs, b, bs); err != nil { + t.Fatal(err) + return + } + var au ApplyUpdate + cs, au = ApplyBlock(cs, b, bs, db.ancestorTimestamp(b.ParentID)) + db.applyBlock(au) + au.ForEachSiacoinElement(func(sce types.SiacoinElement, created, _ bool) { + if created && sce.SiacoinOutput.Address == addr { + subsidy = sce + exists = true + } + }) + return + } + + // receive initial subsidy + initialSubsidy, ok := mineBlock(nil) + if !ok { + t.Fatal("expected subsidy") + } + + // mine until we receive a normal subsidy + for range subsidyInterval - 1 { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + subsidy, ok := mineBlock(nil) + if !ok { + t.Fatal("expected subsidy") + } else if subsidy.SiacoinOutput.Value != initialSubsidy.SiacoinOutput.Value.Div64(12) { + t.Fatal("expected subsidy to be 1/12 of initial subsidy") + } + // disable subsidy + txn := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[scoid], + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: db.sces[scoid].SiacoinOutput.Value, + }}, + NewFoundationAddress: &types.VoidAddress, + } + txn.SiacoinInputs[0].SatisfiedPolicy.Signatures = []types.Signature{key.SignHash(cs.InputSigHash(txn))} + scoid = txn.SiacoinOutputID(txn.ID(), 0) + mineBlock([]types.V2Transaction{txn}) + + // mine until we would receive another subsidy + for range subsidyInterval { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + + // re-enable subsidy + txn = types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[scoid], + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: db.sces[scoid].SiacoinOutput.Value, + }}, + NewFoundationAddress: &addr, + } + txn.SiacoinInputs[0].SatisfiedPolicy.Signatures = []types.Signature{key.SignHash(cs.InputSigHash(txn))} + mineBlock([]types.V2Transaction{txn}) + + // mine until we would receive another subsidy + for range subsidyInterval - 3 { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + if _, ok := mineBlock(nil); !ok { + t.Fatal("expected subsidy") + } +} diff --git a/consensus/validation.go b/consensus/validation.go index a600fe2..66cb2ce 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -387,7 +387,7 @@ func validateArbitraryData(ms *MidState, txn types.Transaction) error { // check that the transaction is signed by a current key var signed bool for _, sci := range txn.SiacoinInputs { - if uh := sci.UnlockConditions.UnlockHash(); uh != ms.base.FoundationPrimaryAddress && uh != ms.base.FoundationFailsafeAddress { + if uh := sci.UnlockConditions.UnlockHash(); uh != ms.base.FoundationSubsidyAddress && uh != ms.base.FoundationManagementAddress { continue } for _, sig := range txn.Signatures { @@ -838,7 +838,7 @@ func validateFoundationUpdate(ms *MidState, txn types.V2Transaction) error { return nil } for _, in := range txn.SiacoinInputs { - if in.Parent.SiacoinOutput.Address == ms.base.FoundationPrimaryAddress { + if in.Parent.SiacoinOutput.Address == ms.base.FoundationManagementAddress { return nil } }