From 4a72a16f798b5f67a448bde8f2ae4539ef51d76c Mon Sep 17 00:00:00 2001 From: lukechampine Date: Fri, 8 Nov 2024 20:50:13 -0500 Subject: [PATCH 01/45] types: Add EncodePtrCast and DecodePtrCast --- types/encoding.go | 28 ++++++++++++++++++++++++++++ types/encoding_test.go | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/types/encoding.go b/types/encoding.go index 073e7a86..0ecddf54 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -117,6 +117,18 @@ func EncodePtr[T any, P interface { } } +// EncodePtrCast encodes a pointer to an object by casting it to V. +func EncodePtrCast[V interface { + Cast() T + EncoderTo +}, T any](e *Encoder, p *T) { + e.WriteBool(p != nil) + if p != nil { + vp := *(*V)(unsafe.Pointer(p)) + vp.EncodeTo(e) + } +} + // EncodeSlice encodes a slice of objects that implement EncoderTo. func EncodeSlice[T EncoderTo](e *Encoder, s []T) { e.WriteUint64(uint64(len(s))) @@ -254,6 +266,22 @@ func DecodePtr[T any, TP interface { } } +// DecodePtrCast decodes a pointer to an object by casting it to V. +func DecodePtrCast[T interface { + Cast() V +}, TP interface { + *T + DecoderFrom +}, V any](d *Decoder, p **V) { + tp := (**T)(unsafe.Pointer(p)) + if d.ReadBool() { + *tp = new(T) + TP(*tp).DecodeFrom(d) + } else { + *tp = nil + } +} + // DecodeSlice decodes a length-prefixed slice of type T, containing values read // from the decoder. func DecodeSlice[T any, DF interface { diff --git a/types/encoding_test.go b/types/encoding_test.go index 84e670dd..141ac2b7 100644 --- a/types/encoding_test.go +++ b/types/encoding_test.go @@ -11,6 +11,26 @@ import ( "lukechampine.com/frand" ) +func TestEncodePtrCast(t *testing.T) { + var buf bytes.Buffer + e := types.NewEncoder(&buf) + c := types.Siacoins(1) + types.EncodePtrCast[types.V1Currency](e, &c) + types.EncodePtrCast[types.V2Currency](e, &c) + types.EncodePtrCast[types.V2Currency](e, nil) + e.Flush() + var c1, c2, c3 *types.Currency + d := types.NewBufDecoder(buf.Bytes()) + types.DecodePtrCast[types.V1Currency](d, &c1) + types.DecodePtrCast[types.V2Currency](d, &c2) + types.DecodePtrCast[types.V2Currency](d, &c3) + if err := d.Err(); err != nil { + t.Fatal(err) + } else if *c1 != c || *c2 != c || c3 != nil { + t.Fatal("mismatch:", c1, c2, c3) + } +} + func TestEncodeSlice(t *testing.T) { txns := multiproofTxns(10, 10) var buf bytes.Buffer From dae65a3de0ea41d950ccb00e84f85dafac308610 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Fri, 8 Nov 2024 20:50:50 -0500 Subject: [PATCH 02/45] types,gateway: Refactor BlockHeader --- consensus/state.go | 10 ++++---- gateway/encoding.go | 44 ++++++---------------------------- gateway/outline.go | 48 +++++--------------------------------- types/encoding.go | 16 +++++++++++++ types/types.go | 57 ++++++++++++++++++++++++++++++--------------- 5 files changed, 72 insertions(+), 103 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 85757d8f..2db710f4 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -609,7 +609,7 @@ type MidState struct { } func (ms *MidState) siacoinElement(ts V1TransactionSupplement, id types.SiacoinOutputID) (types.SiacoinElement, bool) { - if i, ok := ms.created[types.Hash256(id)]; ok { + if i, ok := ms.created[id]; ok { return ms.sces[i], true } return ts.siacoinElement(id) @@ -714,7 +714,7 @@ func (ts *V1TransactionSupplement) DecodeFrom(d *types.Decoder) { func (ts V1TransactionSupplement) siacoinElement(id types.SiacoinOutputID) (sce types.SiacoinElement, ok bool) { for _, sce := range ts.SiacoinInputs { - if types.SiacoinOutputID(sce.ID) == id { + if sce.ID == id { return sce, true } } @@ -723,7 +723,7 @@ func (ts V1TransactionSupplement) siacoinElement(id types.SiacoinOutputID) (sce func (ts V1TransactionSupplement) siafundElement(id types.SiafundOutputID) (sfe types.SiafundElement, ok bool) { for _, sfe := range ts.SiafundInputs { - if types.SiafundOutputID(sfe.ID) == id { + if sfe.ID == id { return sfe, true } } @@ -732,7 +732,7 @@ func (ts V1TransactionSupplement) siafundElement(id types.SiafundOutputID) (sfe func (ts V1TransactionSupplement) revision(id types.FileContractID) (fce types.FileContractElement, ok bool) { for _, fce := range ts.RevisedFileContracts { - if types.FileContractID(fce.ID) == id { + if fce.ID == id { return fce, true } } @@ -741,7 +741,7 @@ func (ts V1TransactionSupplement) revision(id types.FileContractID) (fce types.F func (ts V1TransactionSupplement) storageProof(id types.FileContractID) (sps V1StorageProofSupplement, ok bool) { for _, sps := range ts.StorageProofs { - if types.FileContractID(sps.FileContract.ID) == id { + if sps.FileContract.ID == id { return sps, true } } diff --git a/gateway/encoding.go b/gateway/encoding.go index 31b44fa8..997029e1 100644 --- a/gateway/encoding.go +++ b/gateway/encoding.go @@ -53,36 +53,6 @@ func (h *Header) decodeFrom(d *types.Decoder) { h.NetAddress = d.ReadString() } -func (h *BlockHeader) encodeTo(e *types.Encoder) { - h.ParentID.EncodeTo(e) - e.WriteUint64(h.Nonce) - e.WriteTime(h.Timestamp) - h.MerkleRoot.EncodeTo(e) -} - -func (h *BlockHeader) decodeFrom(d *types.Decoder) { - h.ParentID.DecodeFrom(d) - h.Nonce = d.ReadUint64() - h.Timestamp = d.ReadTime() - h.MerkleRoot.DecodeFrom(d) -} - -func (h *V2BlockHeader) encodeTo(e *types.Encoder) { - h.Parent.EncodeTo(e) - e.WriteUint64(h.Nonce) - e.WriteTime(h.Timestamp) - h.TransactionsRoot.EncodeTo(e) - h.MinerAddress.EncodeTo(e) -} - -func (h *V2BlockHeader) decodeFrom(d *types.Decoder) { - h.Parent.DecodeFrom(d) - h.Nonce = d.ReadUint64() - h.Timestamp = d.ReadTime() - h.TransactionsRoot.DecodeFrom(d) - h.MinerAddress.DecodeFrom(d) -} - func (ob *V2BlockOutline) encodeTo(e *types.Encoder) { e.WriteUint64(ob.Height) ob.ParentID.EncodeTo(e) @@ -261,12 +231,12 @@ func (r *RPCSendBlk) maxResponseLen() int { return 5e6 } // RPCRelayHeader relays a header. type RPCRelayHeader struct { - Header BlockHeader + Header types.BlockHeader emptyResponse } -func (r *RPCRelayHeader) encodeRequest(e *types.Encoder) { r.Header.encodeTo(e) } -func (r *RPCRelayHeader) decodeRequest(d *types.Decoder) { r.Header.decodeFrom(d) } +func (r *RPCRelayHeader) encodeRequest(e *types.Encoder) { r.Header.EncodeTo(e) } +func (r *RPCRelayHeader) decodeRequest(d *types.Decoder) { r.Header.DecodeFrom(d) } func (r *RPCRelayHeader) maxRequestLen() int { return 32 + 8 + 8 + 32 } // RPCRelayTransactionSet relays a transaction set. @@ -364,13 +334,13 @@ func (r *RPCSendCheckpoint) maxResponseLen() int { return 5e6 + 4e3 } // RPCRelayV2Header relays a v2 block header. type RPCRelayV2Header struct { - Header V2BlockHeader + Header types.BlockHeader emptyResponse } -func (r *RPCRelayV2Header) encodeRequest(e *types.Encoder) { r.Header.encodeTo(e) } -func (r *RPCRelayV2Header) decodeRequest(d *types.Decoder) { r.Header.decodeFrom(d) } -func (r *RPCRelayV2Header) maxRequestLen() int { return 8 + 32 + 8 + 8 + 32 + 32 } +func (r *RPCRelayV2Header) encodeRequest(e *types.Encoder) { r.Header.EncodeTo(e) } +func (r *RPCRelayV2Header) decodeRequest(d *types.Decoder) { r.Header.DecodeFrom(d) } +func (r *RPCRelayV2Header) maxRequestLen() int { return 8 + 32 + 32 + 8 } // RPCRelayV2BlockOutline relays a v2 block outline. type RPCRelayV2BlockOutline struct { diff --git a/gateway/outline.go b/gateway/outline.go index 9bf9873c..15ab8b1a 100644 --- a/gateway/outline.go +++ b/gateway/outline.go @@ -1,7 +1,6 @@ package gateway import ( - "encoding/binary" "time" "go.sia.tech/core/consensus" @@ -9,42 +8,6 @@ import ( "go.sia.tech/core/types" ) -// A BlockHeader contains a Block's non-transaction data. -type BlockHeader struct { - ParentID types.BlockID - Nonce uint64 - Timestamp time.Time - MerkleRoot types.Hash256 -} - -// ID returns a hash that uniquely identifies the block. -func (h BlockHeader) ID() types.BlockID { - buf := make([]byte, 32+8+8+32) - copy(buf[:32], h.ParentID[:]) - binary.LittleEndian.PutUint64(buf[32:], h.Nonce) - binary.LittleEndian.PutUint64(buf[40:], uint64(h.Timestamp.Unix())) - copy(buf[48:], h.MerkleRoot[:]) - return types.BlockID(types.HashBytes(buf)) -} - -// A V2BlockHeader contains a V2Block's non-transaction data. -type V2BlockHeader struct { - Parent types.ChainIndex - Nonce uint64 - Timestamp time.Time - TransactionsRoot types.Hash256 - MinerAddress types.Address -} - -// ID returns a hash that uniquely identifies the block. -func (h V2BlockHeader) ID(cs consensus.State) types.BlockID { - return (&types.Block{ - Nonce: h.Nonce, - Timestamp: h.Timestamp, - V2: &types.V2BlockData{Commitment: cs.Commitment(h.TransactionsRoot, h.MinerAddress)}, - }).ID() -} - // An OutlineTransaction identifies a transaction by its full hash. The actual // transaction data may or may not be present. type OutlineTransaction struct { @@ -75,11 +38,12 @@ func (bo V2BlockOutline) commitment(cs consensus.State) types.Hash256 { // ID returns a hash that uniquely identifies the block. func (bo V2BlockOutline) ID(cs consensus.State) types.BlockID { - return (&types.Block{ - Nonce: bo.Nonce, - Timestamp: bo.Timestamp, - V2: &types.V2BlockData{Commitment: bo.commitment(cs)}, - }).ID() + return types.BlockHeader{ + ParentID: bo.ParentID, + Nonce: bo.Nonce, + Timestamp: bo.Timestamp, + Commitment: bo.commitment(cs), + }.ID() } // Missing returns the hashes of transactions that are missing from the block. diff --git a/types/encoding.go b/types/encoding.go index 0ecddf54..58586630 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -880,6 +880,14 @@ func (b V2BlockData) EncodeTo(e *Encoder) { V2TransactionsMultiproof(b.Transactions).EncodeTo(e) } +// EncodeTo implements types.EncoderTo. +func (h BlockHeader) EncodeTo(e *Encoder) { + h.ParentID.EncodeTo(e) + e.WriteUint64(h.Nonce) + e.WriteTime(h.Timestamp) + h.Commitment.EncodeTo(e) +} + // V1Block provides v1 encoding for Block. type V1Block Block @@ -1357,6 +1365,14 @@ func (b *V2BlockData) DecodeFrom(d *Decoder) { (*V2TransactionsMultiproof)(&b.Transactions).DecodeFrom(d) } +// DecodeFrom implements types.DecoderFrom. +func (h *BlockHeader) DecodeFrom(d *Decoder) { + h.ParentID.DecodeFrom(d) + h.Nonce = d.ReadUint64() + h.Timestamp = d.ReadTime() + h.Commitment.DecodeFrom(d) +} + // DecodeFrom implements types.DecoderFrom. func (b *V1Block) DecodeFrom(d *Decoder) { b.ParentID.DecodeFrom(d) diff --git a/types/types.go b/types/types.go index 981721f9..94bfbd11 100644 --- a/types/types.go +++ b/types/types.go @@ -804,7 +804,26 @@ type V2BlockData struct { Transactions []V2Transaction `json:"transactions"` } -// A Block is a set of transactions grouped under a header. +// A BlockHeader is the preimage of a Block's ID. +type BlockHeader struct { + ParentID BlockID `json:"parentID"` + Nonce uint64 `json:"nonce"` + Timestamp time.Time `json:"timestamp"` + Commitment Hash256 `json:"commitment"` +} + +// ID returns the hash of the header data. +func (bh BlockHeader) ID() BlockID { + buf := make([]byte, 32+8+8+32) + copy(buf[:32], bh.ParentID[:]) + binary.LittleEndian.PutUint64(buf[32:], bh.Nonce) + binary.LittleEndian.PutUint64(buf[40:], uint64(bh.Timestamp.Unix())) + copy(buf[48:], bh.Commitment[:]) + return BlockID(HashBytes(buf)) +} + +// A Block is a timestamped set of transactions, immutably linked to a previous +// block, secured by proof-of-work. type Block struct { ParentID BlockID `json:"parentID"` Nonce uint64 `json:"nonce"` @@ -815,12 +834,6 @@ type Block struct { V2 *V2BlockData `json:"v2,omitempty"` } -// MerkleRoot returns the Merkle root of the block's miner payouts and -// transactions. -func (b *Block) MerkleRoot() Hash256 { - return blockMerkleRoot(b.MinerPayouts, b.Transactions) -} - // V2Transactions returns the block's v2 transactions, if present. func (b *Block) V2Transactions() []V2Transaction { if b.V2 != nil { @@ -829,20 +842,26 @@ func (b *Block) V2Transactions() []V2Transaction { return nil } -// ID returns a hash that uniquely identifies a block. -func (b *Block) ID() BlockID { - buf := make([]byte, 32+8+8+32) - binary.LittleEndian.PutUint64(buf[32:], b.Nonce) - binary.LittleEndian.PutUint64(buf[40:], uint64(b.Timestamp.Unix())) - if b.V2 != nil { - copy(buf[:32], "sia/id/block|") - copy(buf[48:], b.V2.Commitment[:]) +// Header returns the block's header. +func (b *Block) Header() BlockHeader { + var commitment Hash256 + if b.V2 == nil { + // NOTE: expensive! + commitment = blockMerkleRoot(b.MinerPayouts, b.Transactions) } else { - root := b.MerkleRoot() // NOTE: expensive! - copy(buf[:32], b.ParentID[:]) - copy(buf[48:], root[:]) + commitment = b.V2.Commitment } - return BlockID(HashBytes(buf)) + return BlockHeader{ + ParentID: b.ParentID, + Nonce: b.Nonce, + Timestamp: b.Timestamp, + Commitment: commitment, + } +} + +// ID returns a hash that uniquely identifies a block. +func (b *Block) ID() BlockID { + return b.Header().ID() } func unmarshalHex(dst []byte, data []byte) error { From 788be94aee30366abf2dc5b5371718e61d9d383f Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 20 Nov 2024 23:50:25 -0500 Subject: [PATCH 03/45] types: Move complexity check to (SpendPolicy).Verify --- types/encoding.go | 9 +-------- types/policy.go | 35 +++++++++++++++++++++++------------ types/policy_test.go | 2 +- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/types/encoding.go b/types/encoding.go index 58586630..0c8677df 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -1113,10 +1113,7 @@ func (txn *Transaction) DecodeFrom(d *Decoder) { // DecodeFrom implements types.DecoderFrom. func (p *SpendPolicy) DecodeFrom(d *Decoder) { - const ( - version = 1 - maxPolicies = 1024 - ) + const version = 1 const ( opInvalid = iota opAbove @@ -1128,7 +1125,6 @@ func (p *SpendPolicy) DecodeFrom(d *Decoder) { opUnlockConditions ) - var totalPolicies int var readPolicy func() (SpendPolicy, error) readPolicy = func() (SpendPolicy, error) { switch op := d.ReadUint8(); op { @@ -1147,9 +1143,6 @@ func (p *SpendPolicy) DecodeFrom(d *Decoder) { case opThreshold: n := d.ReadUint8() of := make([]SpendPolicy, d.ReadUint8()) - if totalPolicies += len(of); totalPolicies > maxPolicies { - return SpendPolicy{}, errors.New("policy is too complex") - } var err error for i := range of { if of[i], err = readPolicy(); err != nil { diff --git a/types/policy.go b/types/policy.go index 794dc85e..ec6e7242 100644 --- a/types/policy.go +++ b/types/policy.go @@ -132,8 +132,9 @@ func (p SpendPolicy) Verify(height uint64, medianTimestamp time.Time, sigHash Ha } return } - errInvalidSignature := errors.New("invalid signature") - errInvalidPreimage := errors.New("invalid preimage") + const maxPolicies = 1024 + var totalPolicies int + errOpaque := errors.New("opaque policy") var verify func(SpendPolicy) error verify = func(p SpendPolicy) error { switch p := p.Type.(type) { @@ -151,28 +152,38 @@ func (p SpendPolicy) Verify(height uint64, medianTimestamp time.Time, sigHash Ha if sig, ok := nextSig(); ok && PublicKey(p).VerifyHash(sigHash, sig) { return nil } - return errInvalidSignature + return errors.New("invalid signature") case PolicyTypeHash: if preimage, ok := nextPreimage(); ok && p == sha256.Sum256(preimage[:]) { return nil } - return errInvalidPreimage + return errors.New("invalid preimage") case PolicyTypeThreshold: - for i := 0; i < len(p.Of) && p.N > 0 && len(p.Of[i:]) >= int(p.N); i++ { - if _, ok := p.Of[i].Type.(PolicyTypeUnlockConditions); ok { + if totalPolicies += len(p.Of); totalPolicies > maxPolicies || len(p.Of) > 255 { + return errors.New("policy is too complex") + } + var satisfied uint8 + for _, sp := range p.Of { + switch sp.Type.(type) { + case PolicyTypeUnlockConditions: return errors.New("unlock conditions cannot be sub-policies") - } else if err := verify(p.Of[i]); err == errInvalidSignature || err == errInvalidPreimage { - return err // fatal; should have been opaque - } else if err == nil { - p.N-- + case PolicyTypeOpaque: + continue + default: + if satisfied == p.N { + return errors.New("threshold exceeded") + } else if err := verify(sp); err != nil { + return err // fatal; should have been opaque + } + satisfied++ } } - if p.N == 0 { + if satisfied == p.N { return nil } return errors.New("threshold not reached") case PolicyTypeOpaque: - return errors.New("opaque policy") + return errOpaque case PolicyTypeUnlockConditions: if err := verify(PolicyAbove(p.Timelock)); err != nil { return err diff --git a/types/policy_test.go b/types/policy_test.go index 1ec8179a..8e3f700e 100644 --- a/types/policy_test.go +++ b/types/policy_test.go @@ -264,7 +264,7 @@ func TestPolicyVerify(t *testing.T) { if err := test.p.Verify(test.height, time.Time{}, sigHash, test.sigs, nil); err != nil && test.valid { t.Fatalf("%v: %v", test.desc, err) } else if err == nil && !test.valid { - t.Fatal("expected error") + t.Fatalf("%v: expected error", test.desc) } } } From bf93a07f6ba2b272ca6c8cb6d2af221f1f1baca7 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 26 Nov 2024 14:38:15 -0500 Subject: [PATCH 04/45] types: Fully test (SpendPolicy).Verify --- types/policy.go | 4 +- types/policy_test.go | 307 ++++++++++++++++++++++++++----------------- 2 files changed, 185 insertions(+), 126 deletions(-) diff --git a/types/policy.go b/types/policy.go index ec6e7242..94629de2 100644 --- a/types/policy.go +++ b/types/policy.go @@ -142,12 +142,12 @@ func (p SpendPolicy) Verify(height uint64, medianTimestamp time.Time, sigHash Ha if height >= uint64(p) { return nil } - return fmt.Errorf("height not above %v", uint64(p)) + return fmt.Errorf("height (%v) not above %v", height, uint64(p)) case PolicyTypeAfter: if medianTimestamp.After(time.Time(p)) { return nil } - return fmt.Errorf("median timestamp not after %v", time.Time(p)) + return fmt.Errorf("median timestamp (%v) not after %v", medianTimestamp, time.Time(p)) case PolicyTypePublicKey: if sig, ok := nextSig(); ok && PublicKey(p).VerifyHash(sigHash, sig) { return nil diff --git a/types/policy_test.go b/types/policy_test.go index 8e3f700e..b3a5de6e 100644 --- a/types/policy_test.go +++ b/types/policy_test.go @@ -2,9 +2,11 @@ package types import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "reflect" + "strings" "testing" "time" @@ -35,236 +37,293 @@ func TestPolicyVerify(t *testing.T) { key := GeneratePrivateKey() pk := key.PublicKey() sigHash := Hash256{1, 2, 3} - + currentTime := CurrentTimestamp() for _, test := range []struct { - desc string - p SpendPolicy - height uint64 - sigs []Signature - valid bool + desc string + p SpendPolicy + height uint64 + sigs []Signature + preimages [][32]byte + err string }{ { - "above 0", - PolicyAbove(0), - 0, - nil, - true, + desc: "above 0", + p: PolicyAbove(0), }, { - "below 1", - PolicyAbove(1), - 0, - nil, - false, + desc: "below 1", + p: PolicyAbove(1), + err: "not above 1", }, { - "above 1", - PolicyAbove(1), - 1, - nil, - true, + desc: "above 1", + p: PolicyAbove(1), + height: 1, }, { - "no signature", - PolicyPublicKey(pk), - 1, - nil, - false, + desc: "after now", + p: PolicyAfter(currentTime), + err: "not after", }, { - "invalid signature", - PolicyPublicKey(pk), - 1, - []Signature{key.SignHash(Hash256{})}, - false, + desc: "after before", + p: PolicyAfter(currentTime.Add(-time.Second)), }, { - "valid signature", - PolicyPublicKey(pk), - 1, - []Signature{key.SignHash(sigHash)}, - true, + desc: "opaque", + p: PolicyOpaque(AnyoneCanSpend()), + err: "opaque", }, { - "valid signature, invalid height", - PolicyThreshold(2, []SpendPolicy{ + desc: "no signature", + p: PolicyPublicKey(pk), + err: "invalid signature", + }, + { + desc: "invalid signature", + p: PolicyPublicKey(pk), + sigs: []Signature{key.SignHash(Hash256{})}, + err: "invalid signature", + }, + { + desc: "valid signature", + p: PolicyPublicKey(pk), + sigs: []Signature{key.SignHash(sigHash)}, + }, + { + desc: "invalid preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 2}}, + err: "invalid preimage", + }, + { + desc: "valid preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 1}}, + }, + { + desc: "superfluous preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 1}, {31: 1}}, + err: "superfluous preimage(s)", + }, + { + desc: "valid signature, invalid height", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 1, - []Signature{key.SignHash(sigHash)}, - false, + sigs: []Signature{key.SignHash(sigHash)}, + err: "not above 10", }, { - "valid height, invalid signature", - PolicyThreshold(2, []SpendPolicy{ + desc: "valid height, invalid signature", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - nil, - false, + height: 11, + err: "invalid signature", }, { - "valid height, valid signature", - PolicyThreshold(2, []SpendPolicy{ + desc: "valid height, valid signature", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - true, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "lower threshold, valid height", - PolicyThreshold(1, []SpendPolicy{ + desc: "lower threshold, valid height", + p: PolicyThreshold(1, []SpendPolicy{ PolicyAbove(10), PolicyOpaque(PolicyPublicKey(pk)), }), - 11, - nil, - true, + height: 11, }, { - "lower threshold, valid signature", - PolicyThreshold(1, []SpendPolicy{ + desc: "lower threshold, valid signature", + p: PolicyThreshold(1, []SpendPolicy{ PolicyOpaque(PolicyAbove(10)), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - true, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "exceed threshold", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold", + p: PolicyThreshold(1, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold exceeded", }, { - "exceed threshold with keys", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold with keys", + p: PolicyThreshold(1, []SpendPolicy{ PolicyPublicKey(pk), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, + err: "threshold exceeded", }, { - "lower threshold, neither valid", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold with above", + p: PolicyThreshold(1, []SpendPolicy{ + PolicyAbove(10), + PolicyAfter(currentTime.Add(-time.Second)), + }), + height: 11, + err: "threshold exceeded", + }, + { + desc: "opaque above subpolicy", + p: PolicyThreshold(1, []SpendPolicy{ + PolicyOpaque(PolicyAbove(10)), + PolicyAfter(currentTime.Add(-time.Second)), + }), + }, + { + desc: "lower threshold, neither valid", + p: PolicyThreshold(1, []SpendPolicy{ PolicyOpaque(PolicyAbove(10)), PolicyOpaque(PolicyPublicKey(pk)), }), - 11, - []Signature{key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", + }, + { + desc: "too many subpolicies", + p: PolicyThreshold(1, make([]SpendPolicy, 256)), + err: "too complex", }, { - "unlock conditions within threshold", - PolicyThreshold(1, []SpendPolicy{ + desc: "too many cumulative subpolicies", + p: PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyAbove(0), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + err: "too complex", + }, + { + desc: "unlock conditions within threshold", + p: PolicyThreshold(1, []SpendPolicy{ {PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 1, }}, }), - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "unlock conditions cannot be sub-policies", }, { - "unlock conditions, invalid height", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, invalid height", + p: SpendPolicy{PolicyTypeUnlockConditions{ Timelock: 10, }}, - 1, - nil, - false, + err: "not above 10", }, { - "unlock conditions, insufficient signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, insufficient signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ SignaturesRequired: 1000, }}, - 1, - nil, - false, + height: 1, + sigs: nil, + err: "threshold not reached", }, { - "unlock conditions, superfluous signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, superfluous signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ SignaturesRequired: 0, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "superfluous signature(s)", }, { - "unlock conditions, wrong signature algorithm", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, wrong signature algorithm", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{{ Algorithm: SpecifierEntropy, Key: nil, }}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "entropy public key", + }, + { + desc: "unlock conditions, unknown signature algorithm", + p: SpendPolicy{PolicyTypeUnlockConditions{ + PublicKeys: []UnlockKey{{ + Algorithm: NewSpecifier("trust me bro"), + }}, + SignaturesRequired: 1, + }}, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "unlock conditions, wrong pubkey", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, wrong pubkey", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{{ Algorithm: SpecifierEd25519, Key: nil, }}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", }, { - "unlock conditions, insufficient signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, insufficient signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 2, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", }, { - "unlock conditions, valid", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, valid", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - true, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "unlock conditions, valid with extra pubkeys", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, valid with extra pubkeys", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey(), PublicKey{1, 2, 3}.UnlockKey(), pk.UnlockKey()}, SignaturesRequired: 2, }}, - 1, - []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, - true, + height: 1, + sigs: []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, }, } { - if err := test.p.Verify(test.height, time.Time{}, sigHash, test.sigs, nil); err != nil && test.valid { + if err := test.p.Verify(test.height, currentTime, sigHash, test.sigs, test.preimages); test.err == "" && err != nil { t.Fatalf("%v: %v", test.desc, err) - } else if err == nil && !test.valid { - t.Fatalf("%v: expected error", test.desc) + } else if test.err != "" && (err == nil || !strings.Contains(err.Error(), test.err)) { + t.Fatalf("%v: expected error containing %q, got %v", test.desc, test.err, err) } } } From ab2ae57dc764c214935f5b6b1b2dd369469516dd Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 27 Nov 2024 14:23:42 -0500 Subject: [PATCH 05/45] consensus: Allow Foundation subsidy to be waived --- consensus/state.go | 9 +++++---- consensus/update.go | 4 +++- consensus/validation.go | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 2db710f4..b7ee343f 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -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) { + if s.FoundationPrimaryAddress == types.VoidAddress { + return types.SiacoinOutput{}, false + } sco.Address = s.FoundationPrimaryAddress - 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. diff --git a/consensus/update.go b/consensus/update.go index a383a817..ce31a38a 100644 --- a/consensus/update.go +++ b/consensus/update.go @@ -569,7 +569,9 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { } if txn.NewFoundationAddress != nil { ms.foundationPrimary = *txn.NewFoundationAddress - ms.foundationFailsafe = *txn.NewFoundationAddress + if *txn.NewFoundationAddress != types.VoidAddress { + ms.foundationFailsafe = *txn.NewFoundationAddress + } } } diff --git a/consensus/validation.go b/consensus/validation.go index 566227df..13180ad7 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -861,7 +861,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.FoundationFailsafeAddress { return nil } } From f32a6e7a00845ffca0c4a066ae60b5f2c5159f6e Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 27 Nov 2024 14:36:06 -0500 Subject: [PATCH 06/45] consensus: Rename Foundation addresses --- consensus/state.go | 70 ++++++++++++++++++++--------------------- consensus/update.go | 15 +++++---- consensus/validation.go | 4 +-- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index b7ee343f..468d8101 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) @@ -258,10 +258,10 @@ func (s State) AncestorDepth() uint64 { // FoundationSubsidy returns the Foundation subsidy output for the child block. func (s State) FoundationSubsidy() (sco types.SiacoinOutput, exists bool) { - if s.FoundationPrimaryAddress == types.VoidAddress { + if s.FoundationSubsidyAddress == types.VoidAddress { return types.SiacoinOutput{}, false } - sco.Address = s.FoundationPrimaryAddress + sco.Address = s.FoundationSubsidyAddress subsidyPerBlock := types.Siacoins(30000) blocksPerYear := uint64(365 * 24 * time.Hour / s.BlockInterval()) blocksPerMonth := blocksPerYear / 12 @@ -589,16 +589,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 @@ -651,16 +651,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 ce31a38a..873e2145 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 } } } @@ -568,9 +568,12 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { }) } if txn.NewFoundationAddress != nil { - ms.foundationPrimary = *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.foundationFailsafe = *txn.NewFoundationAddress + ms.foundationManagement = *txn.NewFoundationAddress } } } @@ -740,8 +743,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/validation.go b/consensus/validation.go index 13180ad7..631e5466 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 { @@ -861,7 +861,7 @@ func validateFoundationUpdate(ms *MidState, txn types.V2Transaction) error { return nil } for _, in := range txn.SiacoinInputs { - if in.Parent.SiacoinOutput.Address == ms.base.FoundationFailsafeAddress { + if in.Parent.SiacoinOutput.Address == ms.base.FoundationManagementAddress { return nil } } From 80d3b2a64bc1d14d543d31fc82fbda8058979f6d Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 28 Nov 2024 20:52:05 -0500 Subject: [PATCH 07/45] consensus: Add TestFoundationSubsidy --- consensus/state.go | 3 + consensus/update_test.go | 120 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/consensus/state.go b/consensus/state.go index 468d8101..6b861df8 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -343,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) } diff --git a/consensus/update_test.go b/consensus/update_test.go index 6cba2adb..c20280a7 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") + } +} From 7980b61da69860f26597749cfdfa2a42ca0a9ee1 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Mon, 2 Dec 2024 16:47:17 +0100 Subject: [PATCH 08/45] v4: remove duration parameter from RPCWriteSectorRequest --- rhp/v4/encoding.go | 2 -- rhp/v4/rhp.go | 1 - rhp/v4/validation.go | 6 +----- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 720babaa..238986a4 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -510,13 +510,11 @@ func (r *RPCReadSectorResponse) maxLen() int { func (r RPCWriteSectorRequest) encodeTo(e *types.Encoder) { r.Prices.EncodeTo(e) r.Token.encodeTo(e) - e.WriteUint64(r.Duration) e.WriteUint64(r.DataLength) } func (r *RPCWriteSectorRequest) decodeFrom(d *types.Decoder) { r.Prices.DecodeFrom(d) r.Token.decodeFrom(d) - r.Duration = d.ReadUint64() r.DataLength = d.ReadUint64() } func (r *RPCWriteSectorRequest) maxLen() int { diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 496c5aa6..e501513e 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -425,7 +425,6 @@ type ( RPCWriteSectorRequest struct { Prices HostPrices `json:"prices"` Token AccountToken `json:"token"` - Duration uint64 `json:"duration"` DataLength uint64 `json:"dataLength"` // extended to SectorSize by host } diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go index d3380594..a2d6377a 100644 --- a/rhp/v4/validation.go +++ b/rhp/v4/validation.go @@ -26,17 +26,13 @@ func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { } // Validate validates a write sector request. -func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey, maxDuration uint64) error { +func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } else if err := req.Token.Validate(); err != nil { return fmt.Errorf("token is invalid: %w", err) } switch { - case req.Duration == 0: - return errors.New("duration must be greater than 0") - case req.Duration > maxDuration: - return fmt.Errorf("duration exceeds maximum: %d > %d", req.Duration, maxDuration) case req.DataLength == 0: return errors.New("sector must not be empty") case req.DataLength%LeafSize != 0: From 2cae9aabf4cab33770b14b65f4cba0d71d792d16 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 19 Nov 2024 23:17:11 -0500 Subject: [PATCH 09/45] consensus: Rename SiafundPool -> SiafundTaxRevenue --- consensus/state.go | 30 +++++++++++++++--------------- consensus/update.go | 12 ++++++------ types/types.go | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 2db710f4..f34b3e6b 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -91,11 +91,11 @@ func (n *Network) GenesisState() State { return State{ Network: n, - Index: types.ChainIndex{Height: ^uint64(0)}, - PrevTimestamps: [11]time.Time{}, - Depth: intToTarget(maxTarget), - ChildTarget: n.InitialTarget, - SiafundPool: types.ZeroCurrency, + Index: types.ChainIndex{Height: ^uint64(0)}, + PrevTimestamps: [11]time.Time{}, + Depth: intToTarget(maxTarget), + ChildTarget: n.InitialTarget, + SiafundTaxRevenue: types.ZeroCurrency, OakTime: 0, OakTarget: intToTarget(maxTarget), @@ -111,11 +111,11 @@ func (n *Network) GenesisState() State { type State struct { Network *Network `json:"-"` // network parameters are not encoded - Index types.ChainIndex `json:"index"` - PrevTimestamps [11]time.Time `json:"prevTimestamps"` // newest -> oldest - Depth types.BlockID `json:"depth"` - ChildTarget types.BlockID `json:"childTarget"` - SiafundPool types.Currency `json:"siafundPool"` + Index types.ChainIndex `json:"index"` + PrevTimestamps [11]time.Time `json:"prevTimestamps"` // newest -> oldest + Depth types.BlockID `json:"depth"` + ChildTarget types.BlockID `json:"childTarget"` + SiafundTaxRevenue types.Currency `json:"siafundTaxRevenue"` // Oak hardfork state OakTime time.Duration `json:"oakTime"` @@ -139,7 +139,7 @@ func (s State) EncodeTo(e *types.Encoder) { } s.Depth.EncodeTo(e) s.ChildTarget.EncodeTo(e) - types.V2Currency(s.SiafundPool).EncodeTo(e) + types.V2Currency(s.SiafundTaxRevenue).EncodeTo(e) e.WriteUint64(uint64(s.OakTime)) s.OakTarget.EncodeTo(e) @@ -160,7 +160,7 @@ func (s *State) DecodeFrom(d *types.Decoder) { } s.Depth.DecodeFrom(d) s.ChildTarget.DecodeFrom(d) - (*types.V2Currency)(&s.SiafundPool).DecodeFrom(d) + (*types.V2Currency)(&s.SiafundTaxRevenue).DecodeFrom(d) s.OakTime = time.Duration(d.ReadUint64()) s.OakTarget.DecodeFrom(d) @@ -533,7 +533,7 @@ func (s State) PartialSigHash(txn types.Transaction, cf types.CoveredFields) typ // TransactionsCommitment returns the commitment hash covering the transactions // that comprise a child block. -func (s *State) TransactionsCommitment(txns []types.Transaction, v2txns []types.V2Transaction) types.Hash256 { +func (s State) TransactionsCommitment(txns []types.Transaction, v2txns []types.V2Transaction) types.Hash256 { var acc blake2b.Accumulator for _, txn := range txns { acc.AddLeaf(txn.FullHash()) @@ -595,7 +595,7 @@ type MidState struct { res map[types.FileContractID]bool v2revs map[types.FileContractID]*types.V2FileContractElement v2res map[types.FileContractID]types.V2FileContractResolutionType - siafundPool types.Currency + siafundTaxRevenue types.Currency foundationPrimary types.Address foundationFailsafe types.Address @@ -657,7 +657,7 @@ func NewMidState(s State) *MidState { res: make(map[types.FileContractID]bool), v2revs: make(map[types.FileContractID]*types.V2FileContractElement), v2res: make(map[types.FileContractID]types.V2FileContractResolutionType), - siafundPool: s.SiafundPool, + siafundTaxRevenue: s.SiafundTaxRevenue, foundationPrimary: s.FoundationPrimaryAddress, foundationFailsafe: s.FoundationFailsafeAddress, } diff --git a/consensus/update.go b/consensus/update.go index a383a817..a2f3414a 100644 --- a/consensus/update.go +++ b/consensus/update.go @@ -367,7 +367,7 @@ func (ms *MidState) addSiafundElement(id types.SiafundOutputID, sfo types.Siafun StateElement: types.StateElement{LeafIndex: types.UnassignedLeafIndex}, ID: id, SiafundOutput: sfo, - ClaimStart: ms.siafundPool, + ClaimStart: ms.siafundTaxRevenue, } ms.sfes = append(ms.sfes, sfe) ms.created[ms.sfes[len(ms.sfes)-1].ID] = len(ms.sfes) - 1 @@ -389,7 +389,7 @@ func (ms *MidState) addFileContractElement(id types.FileContractID, fc types.Fil } ms.fces = append(ms.fces, fce) ms.created[ms.fces[len(ms.fces)-1].ID] = len(ms.fces) - 1 - ms.siafundPool = ms.siafundPool.Add(ms.base.FileContractTax(fce.FileContract)) + ms.siafundTaxRevenue = ms.siafundTaxRevenue.Add(ms.base.FileContractTax(fce.FileContract)) } func (ms *MidState) reviseFileContractElement(fce types.FileContractElement, rev types.FileContract) { @@ -426,7 +426,7 @@ func (ms *MidState) addV2FileContractElement(id types.FileContractID, fc types.V } ms.v2fces = append(ms.v2fces, fce) ms.created[ms.v2fces[len(ms.v2fces)-1].ID] = len(ms.v2fces) - 1 - ms.siafundPool = ms.siafundPool.Add(ms.base.V2FileContractTax(fce.V2FileContract)) + ms.siafundTaxRevenue = ms.siafundTaxRevenue.Add(ms.base.V2FileContractTax(fce.V2FileContract)) } func (ms *MidState) reviseV2FileContractElement(fce types.V2FileContractElement, rev types.V2FileContract) { @@ -477,7 +477,7 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp if !ok { panic("missing SiafundElement") } - claimPortion := ms.siafundPool.Sub(sfe.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfe.SiafundOutput.Value) + claimPortion := ms.siafundTaxRevenue.Sub(sfe.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfe.SiafundOutput.Value) ms.spendSiafundElement(sfe, txid) ms.addImmatureSiacoinElement(sfi.ParentID.ClaimOutputID(), types.SiacoinOutput{Value: claimPortion, Address: sfi.ClaimAddress}) } @@ -528,7 +528,7 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { } for _, sfi := range txn.SiafundInputs { ms.spendSiafundElement(sfi.Parent, txid) - claimPortion := ms.siafundPool.Sub(sfi.Parent.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfi.Parent.SiafundOutput.Value) + claimPortion := ms.siafundTaxRevenue.Sub(sfi.Parent.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfi.Parent.SiafundOutput.Value) ms.addImmatureSiacoinElement(sfi.Parent.ID.V2ClaimOutputID(), types.SiacoinOutput{Value: claimPortion, Address: sfi.ClaimAddress}) } for i, sfo := range txn.SiafundOutputs { @@ -736,7 +736,7 @@ func ApplyBlock(s State, b types.Block, bs V1BlockSupplement, targetTimestamp ti ms := NewMidState(s) ms.ApplyBlock(b, bs) - s.SiafundPool = ms.siafundPool + s.SiafundTaxRevenue = ms.siafundTaxRevenue s.Attestations += uint64(len(ms.aes)) s.FoundationPrimaryAddress = ms.foundationPrimary s.FoundationFailsafeAddress = ms.foundationFailsafe diff --git a/types/types.go b/types/types.go index 94bfbd11..40a81ae8 100644 --- a/types/types.go +++ b/types/types.go @@ -641,7 +641,7 @@ type SiafundElement struct { ID SiafundOutputID `json:"id"` StateElement StateElement `json:"stateElement"` SiafundOutput SiafundOutput `json:"siafundOutput"` - ClaimStart Currency `json:"claimStart"` // value of SiafundPool when element was created + ClaimStart Currency `json:"claimStart"` // value of SiafundTaxRevenue when element was created } // A FileContractElement is a record of a FileContract within the state From d35aec52b16f3394230f524b953b36ca865c442a Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 2 Dec 2024 12:55:23 -0500 Subject: [PATCH 10/45] consensus: Simplify V2FileContractTax --- consensus/state.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index f34b3e6b..908e1a20 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -370,12 +370,7 @@ func (s State) FileContractTax(fc types.FileContract) types.Currency { // V2FileContractTax computes the tax levied on a given v2 contract. func (s State) V2FileContractTax(fc types.V2FileContract) types.Currency { - sum := fc.RenterOutput.Value.Add(fc.HostOutput.Value) - tax := sum.Div64(25) // 4% - // round down to nearest multiple of SiafundCount - _, r := bits.Div64(0, tax.Hi, s.SiafundCount()) - _, r = bits.Div64(r, tax.Lo, s.SiafundCount()) - return tax.Sub(types.NewCurrency64(r)) + return fc.RenterOutput.Value.Add(fc.HostOutput.Value).Div64(25) // 4% } // StorageProofLeafIndex returns the leaf index used when computing or From 51ef10f77ed64f315b75d929c9e1c0300e07e763 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 2 Dec 2024 12:04:28 -0500 Subject: [PATCH 11/45] types: Fix V2FileContractResolution docstring --- types/types.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/types/types.go b/types/types.go index 94bfbd11..5f617b27 100644 --- a/types/types.go +++ b/types/types.go @@ -516,25 +516,19 @@ type V2FileContractRevision struct { } // A V2FileContractResolution closes a v2 file contract's payment channel. There -// are four ways a contract can be resolved: +// are three ways a contract can be resolved: // -// 1) The renter can finalize the contract's current state, preventing further -// revisions and immediately creating its outputs. -// -// 2) The renter and host can jointly renew the contract. The old contract is +// 1) The renter and host can jointly renew the contract. The old contract is // finalized, and a portion of its funds are "rolled over" into a new contract. +// Renewals must be submitted prior to the contract's ProofHeight. // -// 3) The host can submit a storage proof, asserting that it has faithfully -// stored the contract data for the agreed-upon duration. Typically, a storage -// proof is only required if the renter is unable or unwilling to sign a -// renewal. A storage proof can only be submitted after the contract's -// ProofHeight; this allows the renter (or host) to broadcast the -// latest contract revision prior to the proof. +// 2) If the renter is unwilling or unable to sign a renewal, the host can +// submit a storage proof, asserting that it has faithfully stored the contract +// data for the agreed-upon duration. Storage proofs must be submitted after the +// contract's ProofHeight and prior to its ExpirationHeight. // -// 4) Lastly, anyone can submit a contract expiration. An expiration can only -// be submitted after the contract's ExpirationHeight; this gives the host a -// reasonable window of time after the ProofHeight in which to submit a storage -// proof. +// 3) Lastly, anyone can submit a contract expiration. An expiration can only be +// submitted after the contract's ExpirationHeight. // // Once a contract has been resolved, it cannot be altered or resolved again. // When a contract is resolved, its RenterOutput and HostOutput are created From b4d62da4b28c6389bbcc51200332ad70434bcd6b Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 2 Dec 2024 12:10:33 -0500 Subject: [PATCH 12/45] consensus: Require valid signatures on V2FileContractRenewal.NewContract Unlike the FinalRevision, there is no danger of allowing the new contract to be independently valid, as it cannot be broadcast without also being funded. Note that this is already possible with any other v2 contract formation: anyone can "replay" the contract formation in a transaction of their own, but they need to supply the contract funding themselves. --- consensus/validation.go | 2 +- consensus/validation_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/consensus/validation.go b/consensus/validation.go index 566227df..143c4571 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -799,7 +799,7 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("file contract renewal %v does not finalize old contract", i) } else if err := validateRevision(fcr.Parent, old, true); err != nil { return fmt.Errorf("file contract renewal %v final revision %s", i, err) - } else if err := validateContract(renewed, true); err != nil { + } else if err := validateContract(renewed, false); err != nil { return fmt.Errorf("file contract renewal %v initial revision %s", i, err) } diff --git a/consensus/validation_test.go b/consensus/validation_test.go index c5f4f85e..bf3f61af 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -2038,6 +2038,9 @@ func TestV2RenewalResolution(t *testing.T) { test.renewFn(&renewTxn) // sign the renewal + newContract := &renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal).NewContract + newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(*newContract)) + newContract.HostSignature = pk.SignHash(cs.ContractSigHash(*newContract)) sigHash := cs.RenewalSigHash(*resolution) resolution.RenterSignature = pk.SignHash(sigHash) resolution.HostSignature = pk.SignHash(sigHash) From 6a67c9a918dc2f0c99bdd094f5a3020895bee9b2 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 2 Dec 2024 13:55:34 -0500 Subject: [PATCH 13/45] types,consensus: Simplify V2FileContractRenewal Instead of a full revision, renewals now specify just the final outputs and rollover amounts. This sidesteps existing inconsistencies around whether the "final revision" should be inserted into the accumulator or not, as well as the issue of the revision being valid in a standalone transaction. --- consensus/state.go | 1 - consensus/update.go | 4 +- consensus/validation.go | 63 ++++++----------- consensus/validation_test.go | 129 ++++++++++++++++------------------- rhp/v4/rhp.go | 27 ++++---- types/encoding.go | 11 +-- types/types.go | 9 +-- 7 files changed, 101 insertions(+), 143 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 2db710f4..8cd0bfe6 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -574,7 +574,6 @@ func (s State) ContractSigHash(fc types.V2FileContract) types.Hash256 { func (s State) RenewalSigHash(fcr types.V2FileContractRenewal) types.Hash256 { nilSigs( &fcr.NewContract.RenterSignature, &fcr.NewContract.HostSignature, - &fcr.FinalRevision.RenterSignature, &fcr.FinalRevision.HostSignature, &fcr.RenterSignature, &fcr.HostSignature, ) return hashAll("sig/filecontractrenewal", s.v2ReplayPrefix(), fcr) diff --git a/consensus/update.go b/consensus/update.go index a383a817..92370f34 100644 --- a/consensus/update.go +++ b/consensus/update.go @@ -548,9 +548,7 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { var renter, host types.SiacoinOutput switch r := fcr.Resolution.(type) { case *types.V2FileContractRenewal: - renter, host = r.FinalRevision.RenterOutput, r.FinalRevision.HostOutput - renter.Value = renter.Value.Sub(r.RenterRollover) - host.Value = host.Value.Sub(r.HostRollover) + renter, host = r.FinalRenterOutput, r.FinalHostOutput ms.addV2FileContractElement(fce.ID.V2RenewalID(), r.NewContract) case *types.V2StorageProof: renter, host = fc.RenterOutput, fc.HostOutput diff --git a/consensus/validation.go b/consensus/validation.go index 143c4571..a600fe2d 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -690,32 +690,17 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return nil } - validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey, renewal bool) error { - if renewal { - // The sub-contracts of a renewal must have empty signatures; - // otherwise they would be independently valid, i.e. the atomicity - // of the renewal could be violated. Consider a host who has lost or - // deleted their contract data; all they have to do is wait for a - // renter to initiate a renewal, then broadcast just the - // finalization of the old contract, allowing them to successfully - // resolve the contract without a storage proof. - if fc.RenterSignature != (types.Signature{}) { - return errors.New("has non-empty renter signature") - } else if fc.HostSignature != (types.Signature{}) { - return errors.New("has non-empty host signature") - } - } else { - contractHash := ms.base.ContractSigHash(fc) - if !renter.VerifyHash(contractHash, fc.RenterSignature) { - return errors.New("has invalid renter signature") - } else if !host.VerifyHash(contractHash, fc.HostSignature) { - return errors.New("has invalid host signature") - } + validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey) error { + contractHash := ms.base.ContractSigHash(fc) + if !renter.VerifyHash(contractHash, fc.RenterSignature) { + return errors.New("has invalid renter signature") + } else if !host.VerifyHash(contractHash, fc.HostSignature) { + return errors.New("has invalid host signature") } return nil } - validateContract := func(fc types.V2FileContract, renewal bool) error { + validateContract := func(fc types.V2FileContract) error { switch { case fc.Filesize > fc.Capacity: return fmt.Errorf("has filesize (%v) exceeding capacity (%v)", fc.Filesize, fc.Capacity) @@ -730,10 +715,10 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case fc.TotalCollateral.Cmp(fc.HostOutput.Value) > 0: return fmt.Errorf("has total collateral (%d H) exceeding valid host value (%d H)", fc.TotalCollateral, fc.HostOutput.Value) } - return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey, renewal) + return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey) } - validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract, renewal bool) error { + validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract) error { cur := fce.V2FileContract if priorRev, ok := ms.v2revs[fce.ID]; ok { cur = priorRev.V2FileContract @@ -761,11 +746,11 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("leaves no time between proof height (%v) and expiration height (%v)", rev.ProofHeight, rev.ExpirationHeight) } // NOTE: very important that we verify with the *current* keys! - return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey, renewal) + return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey) } for i, fc := range txn.FileContracts { - if err := validateContract(fc, false); err != nil { + if err := validateContract(fc); err != nil { return fmt.Errorf("file contract %v %s", i, err) } } @@ -780,7 +765,7 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { // NOTE: disallowing this means that resolutions always take // precedence over revisions return fmt.Errorf("file contract revision %v resolves contract", i) - } else if err := validateRevision(fcr.Parent, rev, false); err != nil { + } else if err := validateRevision(fcr.Parent, rev); err != nil { return fmt.Errorf("file contract revision %v %s", i, err) } revised[fcr.Parent.ID] = i @@ -794,25 +779,17 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { switch r := fcr.Resolution.(type) { case *types.V2FileContractRenewal: renewal := *r - old, renewed := renewal.FinalRevision, renewal.NewContract - if old.RevisionNumber != types.MaxRevisionNumber { - return fmt.Errorf("file contract renewal %v does not finalize old contract", i) - } else if err := validateRevision(fcr.Parent, old, true); err != nil { - return fmt.Errorf("file contract renewal %v final revision %s", i, err) - } else if err := validateContract(renewed, false); err != nil { - return fmt.Errorf("file contract renewal %v initial revision %s", i, err) - } - rollover := renewal.RenterRollover.Add(renewal.HostRollover) - newContractCost := renewed.RenterOutput.Value.Add(renewed.HostOutput.Value).Add(ms.base.V2FileContractTax(renewed)) - if renewal.RenterRollover.Cmp(old.RenterOutput.Value) > 0 { - return fmt.Errorf("file contract renewal %v has renter rollover (%d H) exceeding old output (%d H)", i, renewal.RenterRollover, old.RenterOutput.Value) - } else if renewal.HostRollover.Cmp(old.HostOutput.Value) > 0 { - return fmt.Errorf("file contract renewal %v has host rollover (%d H) exceeding old output (%d H)", i, renewal.HostRollover, old.HostOutput.Value) - } else if rollover.Cmp(newContractCost) > 0 { + newContractCost := renewal.NewContract.RenterOutput.Value.Add(renewal.NewContract.HostOutput.Value).Add(ms.base.V2FileContractTax(renewal.NewContract)) + if totalRenter := renewal.FinalRenterOutput.Value.Add(renewal.RenterRollover); totalRenter != fc.RenterOutput.Value { + return fmt.Errorf("file contract renewal %v renter payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalRenter, fc.RenterOutput.Value) + } else if totalHost := renewal.FinalHostOutput.Value.Add(renewal.HostRollover); totalHost != fc.HostOutput.Value { + return fmt.Errorf("file contract renewal %v host payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalHost, fc.HostOutput.Value) + } else if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 { return fmt.Errorf("file contract renewal %v has rollover (%d H) exceeding new contract cost (%d H)", i, rollover, newContractCost) + } else if err := validateContract(renewal.NewContract); err != nil { + return fmt.Errorf("file contract renewal %v initial revision %s", i, err) } - renewalHash := ms.base.RenewalSigHash(renewal) if !fc.RenterPublicKey.VerifyHash(renewalHash, renewal.RenterSignature) { return fmt.Errorf("file contract renewal %v has invalid renter signature", i) diff --git a/consensus/validation_test.go b/consensus/validation_test.go index bf3f61af..685504aa 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1453,26 +1453,6 @@ func TestValidateV2Block(t *testing.T) { }} }, }, - { - "file contract renewal that does not finalize old contract", - func(b *types.Block) { - txn := &b.V2.Transactions[0] - txn.SiacoinInputs = []types.V2SiacoinInput{{ - Parent: sces[1], - SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, - }} - - rev := testFces[0].V2FileContract - resolution := types.V2FileContractRenewal{ - FinalRevision: rev, - NewContract: testFces[0].V2FileContract, - } - txn.FileContractResolutions = []types.V2FileContractResolution{{ - Parent: testFces[0], - Resolution: &resolution, - }} - }, - }, { "file contract renewal with invalid final revision", func(b *types.Block) { @@ -1482,12 +1462,9 @@ func TestValidateV2Block(t *testing.T) { SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, }} - rev := testFces[0].V2FileContract - rev.RevisionNumber = types.MaxRevisionNumber - rev.TotalCollateral = types.ZeroCurrency resolution := types.V2FileContractRenewal{ - FinalRevision: rev, - NewContract: testFces[0].V2FileContract, + FinalRenterOutput: types.SiacoinOutput{Value: types.Siacoins(1e6)}, + NewContract: testFces[0].V2FileContract, } txn.FileContractResolutions = []types.V2FileContractResolution{{ Parent: testFces[0], @@ -1506,11 +1483,10 @@ func TestValidateV2Block(t *testing.T) { rev := testFces[0].V2FileContract rev.ExpirationHeight = rev.ProofHeight - finalRev := testFces[0].V2FileContract - finalRev.RevisionNumber = types.MaxRevisionNumber resolution := types.V2FileContractRenewal{ - FinalRevision: finalRev, - NewContract: rev, + FinalRenterOutput: rev.RenterOutput, + FinalHostOutput: rev.HostOutput, + NewContract: rev, } txn.FileContractResolutions = []types.V2FileContractResolution{{ Parent: testFces[0], @@ -1885,7 +1861,6 @@ func TestV2RenewalResolution(t *testing.T) { tests := []struct { desc string renewFn func(*types.V2Transaction) - errors bool errString string }{ { @@ -1896,6 +1871,7 @@ func TestV2RenewalResolution(t *testing.T) { desc: "valid renewal - no renter rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.RenterRollover renewal.RenterRollover = types.ZeroCurrency // subtract the renter cost from the change output txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) @@ -1905,6 +1881,7 @@ func TestV2RenewalResolution(t *testing.T) { desc: "valid renewal - no host rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover renewal.HostRollover = types.ZeroCurrency // subtract the host cost from the change output txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) @@ -1914,19 +1891,39 @@ func TestV2RenewalResolution(t *testing.T) { desc: "valid renewal - partial host rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) - renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) + partial := renewal.NewContract.MissedHostValue.Div64(2) + renewal.FinalHostOutput.Value = partial + renewal.HostRollover = renewal.HostRollover.Sub(partial) // subtract the host cost from the change output - txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract)) }, }, { desc: "valid renewal - partial renter rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) - renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) + partial := renewal.NewContract.RenterOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.RenterRollover = renewal.RenterRollover.Sub(partial) // subtract the host cost from the change output - txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "invalid renewal - bad new contract renter signature", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.RenterSignature[0] ^= 1 }, + errString: "invalid renter signature", + }, + { + desc: "invalid renewal - bad new contract host signature", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.HostSignature[0] ^= 1 + }, + errString: "invalid host signature", }, { desc: "invalid renewal - not enough host funds", @@ -1935,7 +1932,6 @@ func TestV2RenewalResolution(t *testing.T) { renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) // do not adjust the change output }, - errors: true, errString: "do not equal outputs", }, { @@ -1945,7 +1941,6 @@ func TestV2RenewalResolution(t *testing.T) { renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) // do not adjust the change output }, - errors: true, errString: "do not equal outputs", }, { @@ -1963,7 +1958,6 @@ func TestV2RenewalResolution(t *testing.T) { escapeAmount := renewal.HostRollover.Sub(renewal.NewContract.HostOutput.Value) txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) }, - errors: true, errString: "exceeding new contract cost", }, { @@ -1980,18 +1974,12 @@ func TestV2RenewalResolution(t *testing.T) { escapeAmount := renewal.RenterRollover.Sub(renewal.NewContract.RenterOutput.Value) txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) }, - errors: true, errString: "exceeding new contract cost", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - finalRevision := fc - finalRevision.RevisionNumber = types.MaxRevisionNumber - finalRevision.RenterSignature = types.Signature{} - finalRevision.HostSignature = types.Signature{} - - fc := types.V2FileContract{ + newContract := types.V2FileContract{ ProofHeight: 100, ExpirationHeight: 150, RenterPublicKey: pk.PublicKey(), @@ -2004,30 +1992,30 @@ func TestV2RenewalResolution(t *testing.T) { }, MissedHostValue: types.Siacoins(10), } - tax := cs.V2FileContractTax(fc) + newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(newContract)) + newContract.HostSignature = pk.SignHash(cs.ContractSigHash(newContract)) + renewTxn := types.V2Transaction{ - FileContractResolutions: []types.V2FileContractResolution{ - { - Parent: fces[contractID], - Resolution: &types.V2FileContractRenewal{ - FinalRevision: finalRevision, - NewContract: fc, - RenterRollover: types.Siacoins(10), - HostRollover: types.Siacoins(10), - }, + FileContractResolutions: []types.V2FileContractResolution{{ + Parent: fces[contractID], + Resolution: &types.V2FileContractRenewal{ + FinalRenterOutput: types.SiacoinOutput{Address: fc.RenterOutput.Address, Value: types.ZeroCurrency}, + FinalHostOutput: types.SiacoinOutput{Address: fc.HostOutput.Address, Value: types.ZeroCurrency}, + NewContract: newContract, + RenterRollover: types.Siacoins(10), + HostRollover: types.Siacoins(10), }, - }, - SiacoinInputs: []types.V2SiacoinInput{ - { - Parent: genesisOutput, - SatisfiedPolicy: types.SatisfiedPolicy{ - Policy: types.AnyoneCanSpend(), - }, + }}, + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: genesisOutput, + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.AnyoneCanSpend(), }, - }, - SiacoinOutputs: []types.SiacoinOutput{ - {Address: addr, Value: genesisOutput.SiacoinOutput.Value.Sub(tax)}, - }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: genesisOutput.SiacoinOutput.Value.Sub(cs.V2FileContractTax(newContract)), + }}, } resolution, ok := renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) if !ok { @@ -2038,9 +2026,6 @@ func TestV2RenewalResolution(t *testing.T) { test.renewFn(&renewTxn) // sign the renewal - newContract := &renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal).NewContract - newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(*newContract)) - newContract.HostSignature = pk.SignHash(cs.ContractSigHash(*newContract)) sigHash := cs.RenewalSigHash(*resolution) resolution.RenterSignature = pk.SignHash(sigHash) resolution.HostSignature = pk.SignHash(sigHash) @@ -2048,13 +2033,13 @@ func TestV2RenewalResolution(t *testing.T) { ms := NewMidState(cs) err := ValidateV2Transaction(ms, renewTxn) switch { - case test.errors && err == nil: + case test.errString != "" && err == nil: t.Fatal("expected error") - case test.errors && test.errString == "": + case test.errString != "" && test.errString == "": t.Fatalf("received error %q, missing error string to compare", err) - case test.errors && !strings.Contains(err.Error(), test.errString): + case test.errString != "" && !strings.Contains(err.Error(), test.errString): t.Fatalf("expected error %q to contain %q", err, test.errString) - case !test.errors && err != nil: + case test.errString == "" && err != nil: t.Fatalf("unexpected error: %q", err) } }) diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index e501513e..3acc3078 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -661,12 +661,8 @@ func MinRenterAllowance(hp HostPrices, duration uint64, collateral types.Currenc // RenewContract creates a contract renewal for the renew RPC func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContractParams) (types.V2FileContractRenewal, Usage) { var renewal types.V2FileContractRenewal - // clear the old contract - renewal.FinalRevision = fc - renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber - renewal.FinalRevision.FileMerkleRoot = types.Hash256{} - renewal.FinalRevision.RenterSignature = types.Signature{} - renewal.FinalRevision.HostSignature = types.Signature{} + renewal.FinalRenterOutput = fc.RenterOutput + renewal.FinalHostOutput = fc.HostOutput // create the new contract renewal.NewContract = fc @@ -708,6 +704,7 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra } else { renewal.HostRollover = fc.TotalCollateral } + renewal.FinalHostOutput.Value = renewal.FinalHostOutput.Value.Sub(renewal.HostRollover) // if the remaining renter output is greater than the required allowance, // only roll over the new allowance. Otherwise, roll over the remaining @@ -717,6 +714,8 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra } else { renewal.RenterRollover = fc.RenterOutput.Value } + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Sub(renewal.RenterRollover) + return renewal, Usage{ RPC: prices.ContractPrice, Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), @@ -727,12 +726,13 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra // RefreshContract creates a contract renewal for the refresh RPC. func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshContractParams) (types.V2FileContractRenewal, Usage) { var renewal types.V2FileContractRenewal - - // clear the old contract - renewal.FinalRevision = fc - renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber - renewal.FinalRevision.RenterSignature = types.Signature{} - renewal.FinalRevision.HostSignature = types.Signature{} + // roll over everything from the existing contract + renewal.FinalRenterOutput = fc.RenterOutput + renewal.FinalHostOutput = fc.HostOutput + renewal.FinalRenterOutput.Value = types.ZeroCurrency + renewal.FinalHostOutput.Value = types.ZeroCurrency + renewal.HostRollover = fc.HostOutput.Value + renewal.RenterRollover = fc.RenterOutput.Value // create the new contract renewal.NewContract = fc @@ -745,9 +745,6 @@ func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshCo renewal.NewContract.MissedHostValue = fc.MissedHostValue.Add(rp.Collateral) // total collateral includes the additional requested collateral renewal.NewContract.TotalCollateral = fc.TotalCollateral.Add(rp.Collateral) - // roll over everything from the existing contract - renewal.HostRollover = fc.HostOutput.Value - renewal.RenterRollover = fc.RenterOutput.Value return renewal, Usage{ RPC: prices.ContractPrice, Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), diff --git a/types/encoding.go b/types/encoding.go index 0c8677df..28724ac1 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -703,10 +703,11 @@ func (rev V2FileContractRevision) EncodeTo(e *Encoder) { // EncodeTo implements types.EncoderTo. func (ren V2FileContractRenewal) EncodeTo(e *Encoder) { - ren.FinalRevision.EncodeTo(e) - ren.NewContract.EncodeTo(e) + V2SiacoinOutput(ren.FinalRenterOutput).EncodeTo(e) + V2SiacoinOutput(ren.FinalHostOutput).EncodeTo(e) V2Currency(ren.RenterRollover).EncodeTo(e) V2Currency(ren.HostRollover).EncodeTo(e) + ren.NewContract.EncodeTo(e) ren.RenterSignature.EncodeTo(e) ren.HostSignature.EncodeTo(e) } @@ -853,7 +854,6 @@ func (txn V2TransactionSemantics) EncodeTo(e *Encoder) { renewal := *res nilSigs( &renewal.NewContract.RenterSignature, &renewal.NewContract.HostSignature, - &renewal.FinalRevision.RenterSignature, &renewal.FinalRevision.HostSignature, &renewal.RenterSignature, &renewal.HostSignature, ) fcr.Resolution = &renewal @@ -1264,10 +1264,11 @@ func (rev *V2FileContractRevision) DecodeFrom(d *Decoder) { // DecodeFrom implements types.DecoderFrom. func (ren *V2FileContractRenewal) DecodeFrom(d *Decoder) { - ren.FinalRevision.DecodeFrom(d) - ren.NewContract.DecodeFrom(d) + (*V2SiacoinOutput)(&ren.FinalRenterOutput).DecodeFrom(d) + (*V2SiacoinOutput)(&ren.FinalHostOutput).DecodeFrom(d) (*V2Currency)(&ren.RenterRollover).DecodeFrom(d) (*V2Currency)(&ren.HostRollover).DecodeFrom(d) + ren.NewContract.DecodeFrom(d) ren.RenterSignature.DecodeFrom(d) ren.HostSignature.DecodeFrom(d) } diff --git a/types/types.go b/types/types.go index 5f617b27..b41ff69f 100644 --- a/types/types.go +++ b/types/types.go @@ -555,10 +555,11 @@ func (*V2FileContractExpiration) isV2FileContractResolution() {} // A V2FileContractRenewal renews a file contract. type V2FileContractRenewal struct { - FinalRevision V2FileContract `json:"finalRevision"` - NewContract V2FileContract `json:"newContract"` - RenterRollover Currency `json:"renterRollover"` - HostRollover Currency `json:"hostRollover"` + FinalRenterOutput SiacoinOutput `json:"finalRenterOutput"` + FinalHostOutput SiacoinOutput `json:"finalHostOutput"` + RenterRollover Currency `json:"renterRollover"` + HostRollover Currency `json:"hostRollover"` + NewContract V2FileContract `json:"newContract"` // signatures cover above fields RenterSignature Signature `json:"renterSignature"` From 1b3e485f83480d7e9e3e1f54eb9a3e5d03c6ac92 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 2 Dec 2024 16:27:13 -0500 Subject: [PATCH 14/45] consensus: Add TestSiafunds --- consensus/update_test.go | 134 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/consensus/update_test.go b/consensus/update_test.go index 6cba2adb..47f82df2 100644 --- a/consensus/update_test.go +++ b/consensus/update_test.go @@ -1089,11 +1089,6 @@ func TestApplyRevertBlockV2(t *testing.T) { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } - _ = renterPublicKey - _ = hostPublicKey - _ = checkRevertElements - _ = prev - // revert block spending sc and sf ru := RevertBlock(prev, b2, V1BlockSupplement{}) cs = prev @@ -1296,3 +1291,132 @@ func TestApplyRevertBlockV2(t *testing.T) { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } } + +func TestSiafunds(t *testing.T) { + n, genesisBlock := testnet() + n.HardforkV2.AllowHeight = 1 + n.HardforkV2.RequireHeight = 2 + + key := types.GeneratePrivateKey() + + giftAddress := types.StandardAddress(key.PublicKey()) + giftAmountSC := types.Siacoins(100e3) + giftAmountSF := uint64(1000) + giftTxn := types.Transaction{ + SiacoinOutputs: []types.SiacoinOutput{ + {Address: giftAddress, Value: giftAmountSC}, + }, + SiafundOutputs: []types.SiafundOutput{ + {Address: giftAddress, Value: giftAmountSF}, + }, + } + genesisBlock.Transactions = []types.Transaction{giftTxn} + db, cs := newConsensusDB(n, genesisBlock) + + signTxn := func(cs State, txn *types.V2Transaction) { + for i := range txn.SiacoinInputs { + txn.SiacoinInputs[i].SatisfiedPolicy = types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + Signatures: []types.Signature{key.SignHash(cs.InputSigHash(*txn))}, + } + } + for i := range txn.SiafundInputs { + txn.SiafundInputs[i].SatisfiedPolicy = types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + Signatures: []types.Signature{key.SignHash(cs.InputSigHash(*txn))}, + } + } + for i := range txn.FileContracts { + txn.FileContracts[i].RenterSignature = key.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + txn.FileContracts[i].HostSignature = key.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + } + } + mineTxns := func(txns []types.Transaction, v2txns []types.V2Transaction) (au ApplyUpdate, err error) { + b := types.Block{ + ParentID: cs.Index.ID, + Timestamp: types.CurrentTimestamp(), + MinerPayouts: []types.SiacoinOutput{{Address: types.VoidAddress, Value: cs.BlockReward()}}, + Transactions: txns, + } + if len(v2txns) > 0 { + b.V2 = &types.V2BlockData{ + Height: cs.Index.Height + 1, + Commitment: cs.Commitment(cs.TransactionsCommitment(txns, v2txns), b.MinerPayouts[0].Address), + Transactions: v2txns, + } + } + findBlockNonce(cs, &b) + if err = ValidateBlock(cs, b, V1BlockSupplement{}); err != nil { + return + } + cs, au = ApplyBlock(cs, b, V1BlockSupplement{}, db.ancestorTimestamp(b.ParentID)) + db.applyBlock(au) + return + } + + fc := types.V2FileContract{ + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: types.SiacoinOutput{Value: types.Siacoins(5000)}, + HostOutput: types.SiacoinOutput{Value: types.Siacoins(5000)}, + RenterPublicKey: key.PublicKey(), + HostPublicKey: key.PublicKey(), + } + fcValue := fc.RenterOutput.Value.Add(fc.HostOutput.Value).Add(cs.V2FileContractTax(fc)) + + txn := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[giftTxn.SiacoinOutputID(0)], + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: giftAddress, + Value: giftAmountSC.Sub(fcValue), + }}, + FileContracts: []types.V2FileContract{fc}, + } + signTxn(cs, &txn) + prev := cs + if _, err := mineTxns(nil, []types.V2Transaction{txn}); err != nil { + t.Fatal(err) + } + // siafund revenue should have increased + if cs.SiafundTaxRevenue != prev.SiafundTaxRevenue.Add(cs.V2FileContractTax(fc)) { + t.Fatalf("expected %v siafund revenue, got %v", prev.SiafundTaxRevenue.Add(cs.V2FileContractTax(fc)), cs.SiafundTaxRevenue) + } + + // make a siafund claim + txn = types.V2Transaction{ + SiafundInputs: []types.V2SiafundInput{{ + Parent: db.sfes[giftTxn.SiafundOutputID(0)], + ClaimAddress: giftAddress, + }}, + SiafundOutputs: []types.SiafundOutput{{ + Address: giftAddress, + Value: giftAmountSF, + }}, + } + signTxn(cs, &txn) + prev = cs + if au, err := mineTxns(nil, []types.V2Transaction{txn}); err != nil { + t.Fatal(err) + } else { + // siafund revenue should be unchanged + if cs.SiafundTaxRevenue != prev.SiafundTaxRevenue { + t.Fatalf("expected %v siafund revenue, got %v", prev.SiafundTaxRevenue, cs.SiafundTaxRevenue) + } + // should have received a timelocked siafund claim output + var claimOutput *types.SiacoinElement + au.ForEachSiacoinElement(func(sce types.SiacoinElement, _, _ bool) { + if sce.ID == txn.SiafundInputs[0].Parent.ID.V2ClaimOutputID() { + claimOutput = &sce + } + }) + if claimOutput == nil { + t.Fatal("expected siafund claim output") + } else if claimOutput.MaturityHeight != cs.MaturityHeight()-1 { + t.Fatalf("expected siafund claim output to mature at height %v, got %v", cs.MaturityHeight()-1, claimOutput.MaturityHeight) + } else if exp := cs.V2FileContractTax(fc).Div64(cs.SiafundCount() / giftAmountSF); claimOutput.SiacoinOutput.Value != exp { + t.Fatalf("expected siafund claim output value %v, got %v", exp, claimOutput.SiacoinOutput.Value) + } + } +} From 5a7b17f79f80c2bd026e49e54cb4986d7e257049 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 2 Dec 2024 16:22:58 -0800 Subject: [PATCH 15/45] rhp4: use consts for validation functions, fix collateral validation --- .gitignore | 2 ++ rhp/v4/rhp.go | 5 ++++- rhp/v4/validation.go | 36 +++++++++++++++++++++--------------- 3 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2608ec26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.vscode \ No newline at end of file diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 3acc3078..55fb2b3c 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -588,6 +588,9 @@ func RenewalCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal // RefreshCost calculates the cost to the host and renter for refreshing a contract. func RefreshCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal, minerFee types.Currency) (renter, host types.Currency) { renter = r.NewContract.RenterOutput.Value.Add(p.ContractPrice).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) + // the calculation is different from renewal because the host's revenue is also rolled into the refresh. + // This calculates the new collateral the host is expected to put up: + // new collateral = (new revenue + existing revenue + new collateral + existing collateral) - new revenue - (existing revenue + existing collateral) host = r.NewContract.HostOutput.Value.Sub(p.ContractPrice).Sub(r.HostRollover) return } @@ -746,8 +749,8 @@ func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshCo // total collateral includes the additional requested collateral renewal.NewContract.TotalCollateral = fc.TotalCollateral.Add(rp.Collateral) return renewal, Usage{ + // Refresh usage is only the contract price since duration is not increased RPC: prices.ContractPrice, - Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), RiskedCollateral: renewal.NewContract.TotalCollateral.Sub(renewal.NewContract.MissedHostValue), } } diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go index a2d6377a..9ca1dc5e 100644 --- a/rhp/v4/validation.go +++ b/rhp/v4/validation.go @@ -44,11 +44,11 @@ func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey) error { } // Validate validates a modify sectors request. Signatures are not validated. -func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxActions uint64) error { +func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if uint64(len(req.Indices)) > maxActions { - return fmt.Errorf("removing too many sectors at once: %d > %d", len(req.Indices), maxActions) + } else if uint64(len(req.Indices)) > MaxSectorBatchSize { + return fmt.Errorf("removing too many sectors at once: %d > %d", len(req.Indices), MaxSectorBatchSize) } seen := make(map[uint64]bool) sectors := fc.Filesize / SectorSize @@ -64,7 +64,7 @@ func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileCo } // Validate validates a sector roots request. Signatures are not validated. -func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxSectors uint64) error { +func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -75,8 +75,8 @@ func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileCo return errors.New("length must be greater than 0") case req.Length+req.Offset > contractSectors: return fmt.Errorf("read request range exceeds contract sectors: %d > %d", req.Length+req.Offset, contractSectors) - case req.Length > maxSectors: - return fmt.Errorf("read request range exceeds maximum sectors: %d > %d", req.Length, maxSectors) + case req.Length > MaxSectorBatchSize: + return fmt.Errorf("read request range exceeds maximum sectors: %d > %d", req.Length, MaxSectorBatchSize) } return nil } @@ -122,7 +122,7 @@ func (req *RPCFormContractRequest) Validate(pk types.PublicKey, tip types.ChainI } // Validate validates a renew contract request. Prices are not validated -func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.ChainIndex, existingProofHeight uint64, maxCollateral types.Currency, maxDuration uint64) error { +func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.ChainIndex, existingSize uint64, existingProofHeight uint64, maxCollateral types.Currency, maxDuration uint64) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -144,14 +144,18 @@ func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.Chain // calculate the minimum allowance required for the contract based on the // host's locked collateral and the contract duration minRenterAllowance := MinRenterAllowance(hp, duration, req.Renewal.Collateral) + // collateral is risked for the entire contract duration + riskedCollateral := req.Prices.Collateral.Mul64(existingSize).Mul64(expirationHeight - req.Prices.TipHeight) + // renewals add collateral on top of the required risked collateral + totalCollateral := req.Renewal.Collateral.Add(riskedCollateral) switch { case expirationHeight <= tip.Height: // must be validated against tip instead of prices return errors.New("contract expiration height is in the past") case req.Renewal.Allowance.IsZero(): return errors.New("allowance must be greater than zero") - case req.Renewal.Collateral.Cmp(maxCollateral) > 0: - return fmt.Errorf("collateral %v exceeds max collateral %v", req.Renewal.Collateral, maxCollateral) + case totalCollateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("required collateral %v exceeds max collateral %v", totalCollateral, maxCollateral) case duration > maxDuration: return fmt.Errorf("contract duration %v exceeds max duration %v", duration, maxDuration) case req.Renewal.Allowance.Cmp(minRenterAllowance) < 0: @@ -162,7 +166,7 @@ func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.Chain } // Validate validates a refresh contract request. Prices are not validated -func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHeight uint64, maxCollateral types.Currency) error { +func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, existingTotalCollateral types.Currency, expirationHeight uint64, maxCollateral types.Currency) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -180,12 +184,14 @@ func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHei // calculate the minimum allowance required for the contract based on the // host's locked collateral and the contract duration minRenterAllowance := MinRenterAllowance(hp, expirationHeight-req.Prices.TipHeight, req.Refresh.Collateral) + // refreshes add collateral on top of the existing collateral + totalCollateral := req.Refresh.Collateral.Add(existingTotalCollateral) switch { case req.Refresh.Allowance.IsZero(): return errors.New("allowance must be greater than zero") - case req.Refresh.Collateral.Cmp(maxCollateral) > 0: - return fmt.Errorf("collateral %v exceeds max collateral %v", req.Refresh.Collateral, maxCollateral) + case totalCollateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("required collateral %v exceeds max collateral %v", totalCollateral, maxCollateral) case req.Refresh.Allowance.Cmp(minRenterAllowance) < 0: return fmt.Errorf("allowance %v is less than minimum allowance %v", req.Refresh.Allowance, minRenterAllowance) default: @@ -206,13 +212,13 @@ func (req *RPCVerifySectorRequest) Validate(pk types.PublicKey) error { } // Validate checks that the request is valid -func (req *RPCAppendSectorsRequest) Validate(pk types.PublicKey, maxActions uint64) error { +func (req *RPCAppendSectorsRequest) Validate(pk types.PublicKey) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } else if len(req.Sectors) == 0 { return errors.New("no sectors to append") - } else if uint64(len(req.Sectors)) > maxActions { - return fmt.Errorf("too many sectors to append: %d > %d", len(req.Sectors), maxActions) + } else if uint64(len(req.Sectors)) > MaxSectorBatchSize { + return fmt.Errorf("too many sectors to append: %d > %d", len(req.Sectors), MaxSectorBatchSize) } return nil } From 1173eac96be509eadc1e8e7365b8bc4b4b0ca912 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 2 Dec 2024 20:11:05 -0800 Subject: [PATCH 16/45] consensus: fix renewal payout validation --- consensus/validation.go | 22 +++++++++++++++++----- consensus/validation_test.go | 22 ++++++++++++++++++++++ rhp/v4/encoding.go | 4 ++++ rhp/v4/rhp.go | 2 ++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/consensus/validation.go b/consensus/validation.go index 66cb2ce5..61f09e65 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -780,12 +780,24 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case *types.V2FileContractRenewal: renewal := *r + if fc.RenterPublicKey != renewal.NewContract.RenterPublicKey { + return fmt.Errorf("file contract renewal %v changes renter public key", i) + } else if fc.HostPublicKey != renewal.NewContract.HostPublicKey { + return fmt.Errorf("file contract renewal %v changes host public key", i) + } + + // validate that the renewal value is equal to existing contract's value. + // This must be done as a sum of the outputs, since the individual payouts may have + // changed in an unbroadcast revision. + totalPayout := renewal.FinalRenterOutput.Value.Add(renewal.RenterRollover). + Add(renewal.FinalHostOutput.Value).Add(renewal.HostRollover) + existingPayout := fc.RenterOutput.Value.Add(fc.HostOutput.Value) + if totalPayout != existingPayout { + return fmt.Errorf("file contract renewal %d renewal payout (%s) does not match existing contract payout %s", i, totalPayout, existingPayout) + } + newContractCost := renewal.NewContract.RenterOutput.Value.Add(renewal.NewContract.HostOutput.Value).Add(ms.base.V2FileContractTax(renewal.NewContract)) - if totalRenter := renewal.FinalRenterOutput.Value.Add(renewal.RenterRollover); totalRenter != fc.RenterOutput.Value { - return fmt.Errorf("file contract renewal %v renter payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalRenter, fc.RenterOutput.Value) - } else if totalHost := renewal.FinalHostOutput.Value.Add(renewal.HostRollover); totalHost != fc.HostOutput.Value { - return fmt.Errorf("file contract renewal %v host payout plus rollover (%d H) does not match old contract payout (%d H)", i, totalHost, fc.HostOutput.Value) - } else if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 { + if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 { return fmt.Errorf("file contract renewal %v has rollover (%d H) exceeding new contract cost (%d H)", i, rollover, newContractCost) } else if err := validateContract(renewal.NewContract); err != nil { return fmt.Errorf("file contract renewal %v initial revision %s", i, err) diff --git a/consensus/validation_test.go b/consensus/validation_test.go index 685504aa..12fc6717 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1925,6 +1925,28 @@ func TestV2RenewalResolution(t *testing.T) { }, errString: "invalid host signature", }, + { + desc: "invalid renewal - different host key", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + sk := types.GeneratePrivateKey() + renewal.NewContract.HostPublicKey = sk.PublicKey() + contractSigHash := cs.ContractSigHash(renewal.NewContract) + renewal.NewContract.HostSignature = sk.SignHash(contractSigHash) + }, + errString: "changes host public key", + }, + { + desc: "invalid renewal - different renter key", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + sk := types.GeneratePrivateKey() + renewal.NewContract.RenterPublicKey = sk.PublicKey() + contractSigHash := cs.ContractSigHash(renewal.NewContract) + renewal.NewContract.RenterSignature = sk.SignHash(contractSigHash) + }, + errString: "changes renter public key", + }, { desc: "invalid renewal - not enough host funds", renewFn: func(txn *types.V2Transaction) { diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 238986a4..8172400b 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -263,10 +263,12 @@ func (r *RPCRenewContractResponse) maxLen() int { func (r *RPCRenewContractSecondResponse) encodeTo(e *types.Encoder) { r.RenterRenewalSignature.EncodeTo(e) + r.RenterContractSignature.EncodeTo(e) types.EncodeSlice(e, r.RenterSatisfiedPolicies) } func (r *RPCRenewContractSecondResponse) decodeFrom(d *types.Decoder) { r.RenterRenewalSignature.DecodeFrom(d) + r.RenterContractSignature.DecodeFrom(d) types.DecodeSlice(d, &r.RenterSatisfiedPolicies) } func (r *RPCRenewContractSecondResponse) maxLen() int { @@ -331,10 +333,12 @@ func (r *RPCRefreshContractResponse) maxLen() int { func (r *RPCRefreshContractSecondResponse) encodeTo(e *types.Encoder) { r.RenterRenewalSignature.EncodeTo(e) + r.RenterContractSignature.EncodeTo(e) types.EncodeSlice(e, r.RenterSatisfiedPolicies) } func (r *RPCRefreshContractSecondResponse) decodeFrom(d *types.Decoder) { r.RenterRenewalSignature.DecodeFrom(d) + r.RenterContractSignature.DecodeFrom(d) types.DecodeSlice(d, &r.RenterSatisfiedPolicies) } func (r *RPCRefreshContractSecondResponse) maxLen() int { diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 55fb2b3c..3e454c4d 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -310,6 +310,7 @@ type ( // RPCRefreshContractSecondResponse implements Object. RPCRefreshContractSecondResponse struct { RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterContractSignature types.Signature `json:"renterContractSignature"` RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` } // RPCRefreshContractThirdResponse implements Object. @@ -344,6 +345,7 @@ type ( // RPCRenewContractSecondResponse implements Object. RPCRenewContractSecondResponse struct { RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterContractSignature types.Signature `json:"renterContractSignature"` RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` } // RPCRenewContractThirdResponse implements Object. From c6a988d759d6918584c5808f0505288502908d2d Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 2 Dec 2024 20:32:44 -0800 Subject: [PATCH 17/45] consensus: add additional resolution test cases --- consensus/validation_test.go | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/consensus/validation_test.go b/consensus/validation_test.go index 12fc6717..d1d88420 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1909,6 +1909,68 @@ func TestV2RenewalResolution(t *testing.T) { txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract)) }, }, + { + desc: "valid renewal - changed host payout", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the renter payout to the host + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.HostRollover = types.ZeroCurrency + renewal.FinalRenterOutput.Value = renewal.RenterRollover + renewal.RenterRollover = types.ZeroCurrency + partial := renewal.FinalRenterOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.FinalHostOutput.Value = renewal.FinalHostOutput.Value.Add(partial) + // subtract the cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - changed renter payout", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the host payout to the renter + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.HostRollover = types.ZeroCurrency + renewal.FinalRenterOutput.Value = renewal.RenterRollover + renewal.RenterRollover = types.ZeroCurrency + partial := renewal.FinalHostOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Add(partial) + // subtract the cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "invalid renewal - total payout exceeding parent", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the renter payout to the host + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Add(types.Siacoins(1)) + }, + errString: "does not match existing contract payout", + }, + { + desc: "invalid renewal - total payout less than parent", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.RenterRollover.Sub(types.Siacoins(1)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(types.Siacoins(1)).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + errString: "does not match existing contract payout", + }, + { + desc: "invalid renewal - total payout less than parent - no rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.RenterRollover.Sub(types.Siacoins(1)) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.RenterRollover = types.ZeroCurrency + renewal.HostRollover = types.ZeroCurrency + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.FinalRenterOutput.Value).Sub(renewal.FinalHostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + errString: "siacoin inputs (1000000000000000000000000000 H) do not equal outputs (1001000000000000000000000000 H)", // this is an inputs != outputs error because the renewal is validated there first + }, { desc: "invalid renewal - bad new contract renter signature", renewFn: func(txn *types.V2Transaction) { From db3b76157f12d02b5013e79f483bc6576ddd8598 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 4 Dec 2024 08:49:02 -0800 Subject: [PATCH 18/45] automate releases --- .changeset/automate_releases.md | 5 +++ .github/workflows/prepare-release.yml | 26 +++++++++++++ .github/workflows/release.yml | 27 ++++++++++++++ knope.toml | 54 +++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 .changeset/automate_releases.md create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 knope.toml diff --git a/.changeset/automate_releases.md b/.changeset/automate_releases.md new file mode 100644 index 00000000..d5fea6ed --- /dev/null +++ b/.changeset/automate_releases.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +# Automate releases diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..7b6ca01b --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,26 @@ +on: + push: + branches: [master] + +permissions: + contents: write + pull-requests: write + +name: Create Release PR +jobs: + prepare-release: + if: "!contains(github.event.head_commit.message, 'chore: prepare release')" # Skip merges from releases + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config --global user.name github-actions[bot] + git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 + - run: knope prepare-release --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..10b4a009 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +on: + pull_request: + types: + - closed + branches: + - master + +permissions: + contents: write + +name: Create Release PR +jobs: + prepare-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config --global user.name github-actions[bot] + git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 + - run: knope release --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true diff --git a/knope.toml b/knope.toml new file mode 100644 index 00000000..2c1029f4 --- /dev/null +++ b/knope.toml @@ -0,0 +1,54 @@ +[package] +changelog = "CHANGELOG.md" +versioned_files = ["go.mod"] +ignore_go_major_versioning = true + +[[workflows]] +name = "document-change" + +[[workflows.steps]] +type = "CreateChangeFile" + +[[workflows]] +name = "prepare-release" + +[[workflows.steps]] +type = "Command" +command = "git switch -c release" + +[[workflows.steps]] +type = "PrepareRelease" + +[[workflows.steps]] +type = "Command" +command = "git commit -m \"chore: prepare release $version\"" +variables = { "$version" = "Version" } + +[[workflows.steps]] +type = "Command" +command = "git push --force --set-upstream origin release" + +[workflows.steps.variables] +"$version" = "Version" + +[[workflows.steps]] +type = "CreatePullRequest" +base = "master" + +[workflows.steps.title] +template = "chore: prepare release $version" +variables = { "$version" = "Version" } + +[workflows.steps.body] +template = "This PR was created automatically. Merging it will create a new release for $version\n\n$changelog" +variables = { "$changelog" = "ChangelogEntry", "$version" = "Version" } + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "Release" + +[github] +owner = "SiaFoundation" +repo = "core" From 731401499bd225c278b61b0a503f983f7fc5b9d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:55:09 +0000 Subject: [PATCH 19/45] chore: prepare release 0.7.1 --- .changeset/automate_releases.md | 5 ----- CHANGELOG.md | 5 +++++ go.mod | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 .changeset/automate_releases.md create mode 100644 CHANGELOG.md diff --git a/.changeset/automate_releases.md b/.changeset/automate_releases.md deleted file mode 100644 index d5fea6ed..00000000 --- a/.changeset/automate_releases.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -default: patch ---- - -# Automate releases diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b8d1b5f8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.7.1 (2024-12-04) + +### Fixes + +- Automate releases diff --git a/go.mod b/go.mod index b30a5029..8cd53158 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core +module go.sia.tech/core // v0.7.1 go 1.23.1 From c706016e0aedf07c37026638ada2759c978e35bd Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 4 Dec 2024 09:39:24 -0800 Subject: [PATCH 20/45] rename workflows --- .github/workflows/prepare-release.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 7b6ca01b..0a719bd3 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -1,3 +1,4 @@ +name: Prepare Release on: push: branches: [master] @@ -6,7 +7,6 @@ permissions: contents: write pull-requests: write -name: Create Release PR jobs: prepare-release: if: "!contains(github.event.head_commit.message, 'chore: prepare release')" # Skip merges from releases diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10b4a009..47dae242 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,4 @@ +name: Release on: pull_request: types: @@ -8,7 +9,6 @@ on: permissions: contents: write -name: Create Release PR jobs: prepare-release: runs-on: ubuntu-latest From 00790167a2307b057a5392b45ca65eee57af6f7a Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 5 Dec 2024 19:43:33 -0500 Subject: [PATCH 21/45] consensus: Rename update -> application --- consensus/{update.go => application.go} | 0 consensus/{update_test.go => application_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename consensus/{update.go => application.go} (100%) rename consensus/{update_test.go => application_test.go} (100%) diff --git a/consensus/update.go b/consensus/application.go similarity index 100% rename from consensus/update.go rename to consensus/application.go diff --git a/consensus/update_test.go b/consensus/application_test.go similarity index 100% rename from consensus/update_test.go rename to consensus/application_test.go From eaa5ca866bc22692b1b709a3842d5da41c8a5722 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:27:52 +0000 Subject: [PATCH 22/45] build(deps): bump the all-dependencies group with 2 updates Bumps the all-dependencies group with 2 updates: [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/sys](https://github.com/golang/sys). Updates `golang.org/x/crypto` from 0.29.0 to 0.30.0 - [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.30.0) Updates `golang.org/x/sys` from 0.27.0 to 0.28.0 - [Commits](https://github.com/golang/sys/compare/v0.27.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-dependencies - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8cd53158..1939fa5c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.2 require ( go.sia.tech/mux v1.3.0 - golang.org/x/crypto v0.29.0 - golang.org/x/sys v0.27.0 + golang.org/x/crypto v0.30.0 + golang.org/x/sys v0.28.0 lukechampine.com/frand v1.5.1 ) diff --git a/go.sum b/go.sum index 5fffbe42..95a86e94 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= From 6f9994c83aa0fefe0a46c7db855bf6825b189e8e Mon Sep 17 00:00:00 2001 From: lukechampine Date: Thu, 5 Dec 2024 19:12:17 -0500 Subject: [PATCH 23/45] consensus: Allow revisions to set MaxRevisionNumber --- .changesets/allow-max-revision.md | 7 +++++++ consensus/validation.go | 4 ---- consensus/validation_test.go | 7 ------- 3 files changed, 7 insertions(+), 11 deletions(-) create mode 100644 .changesets/allow-max-revision.md diff --git a/.changesets/allow-max-revision.md b/.changesets/allow-max-revision.md new file mode 100644 index 00000000..a9057e02 --- /dev/null +++ b/.changesets/allow-max-revision.md @@ -0,0 +1,7 @@ +--- +default: minor +--- + +# Allow revisions to set MaxRevisionNumber + +`MaxRevisionNumber` was previously used to finalize contracts, but that is not the case anymore, so the restriction can be removed. \ No newline at end of file diff --git a/consensus/validation.go b/consensus/validation.go index 61f09e65..ee5e80dd 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -761,10 +761,6 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("file contract revision %v parent (%v) %s", i, fcr.Parent.ID, err) } else if cur.ProofHeight < ms.base.childHeight() { return fmt.Errorf("file contract revision %v cannot be applied to contract after proof height (%v)", i, cur.ProofHeight) - } else if rev.RevisionNumber == types.MaxRevisionNumber { - // NOTE: disallowing this means that resolutions always take - // precedence over revisions - return fmt.Errorf("file contract revision %v resolves contract", i) } else if err := validateRevision(fcr.Parent, rev); err != nil { return fmt.Errorf("file contract revision %v %s", i, err) } diff --git a/consensus/validation_test.go b/consensus/validation_test.go index d1d88420..c4cbf93e 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1128,13 +1128,6 @@ func TestValidateV2Block(t *testing.T) { txn.NewFoundationAddress = &addr }, }, - { - "revision that resolves contract", - func(b *types.Block) { - txn := &b.V2.Transactions[0] - txn.FileContractRevisions[0].Revision.RevisionNumber = types.MaxRevisionNumber - }, - }, { "revision with window that starts in past", func(b *types.Block) { From f356bb790cb91281a0a7f6883ac803342af8ec92 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 9 Dec 2024 08:35:11 -0800 Subject: [PATCH 24/45] chore: fix changeset, rename workflow --- {.changesets => .changeset}/allow-max-revision.md | 0 .github/workflows/release.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {.changesets => .changeset}/allow-max-revision.md (100%) diff --git a/.changesets/allow-max-revision.md b/.changeset/allow-max-revision.md similarity index 100% rename from .changesets/allow-max-revision.md rename to .changeset/allow-max-revision.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47dae242..444c174a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ permissions: contents: write jobs: - prepare-release: + release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.2.2 From 3f8cf9c39bbc350ee422467e5eaa8342742e6995 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 10 Dec 2024 14:44:38 -0800 Subject: [PATCH 25/45] fix(rhp4): Include storage cost in renter renewal cost --- rhp/v4/rhp.go | 3 +- rhp/v4/rhp_test.go | 210 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 3e454c4d..05d1f164 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -582,7 +582,8 @@ func ContractCost(cs consensus.State, p HostPrices, fc types.V2FileContract, min // RenewalCost calculates the cost to the host and renter for renewing a contract. func RenewalCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal, minerFee types.Currency) (renter, host types.Currency) { - renter = r.NewContract.RenterOutput.Value.Add(p.ContractPrice).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) + contractCost := r.NewContract.HostOutput.Value.Sub(r.NewContract.TotalCollateral) // (contract price + storage cost + locked collateral) - locked collateral + renter = r.NewContract.RenterOutput.Value.Add(contractCost).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) host = r.NewContract.TotalCollateral.Sub(r.HostRollover) return } diff --git a/rhp/v4/rhp_test.go b/rhp/v4/rhp_test.go index ae4f0f00..295094ba 100644 --- a/rhp/v4/rhp_test.go +++ b/rhp/v4/rhp_test.go @@ -3,6 +3,7 @@ package rhp import ( "testing" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" ) @@ -19,3 +20,212 @@ func TestMinRenterAllowance(t *testing.T) { t.Fatalf("expected %v, got %v", expected, minAllowance) } } + +func TestRenewalCost(t *testing.T) { + const ( + initialProofHeight = 1000 + initialExpiration = initialProofHeight + ProofWindow + + renewalHeight = 150 + extension = 10 + renewalProofHeight = initialProofHeight + extension + renewalExpiration = renewalProofHeight + ProofWindow + renewalDuration = renewalExpiration - renewalHeight + ) + cs := consensus.State{} + prices := HostPrices{ + ContractPrice: types.NewCurrency64(100), + Collateral: types.NewCurrency64(200), + StoragePrice: types.NewCurrency64(300), + IngressPrice: types.NewCurrency64(400), + EgressPrice: types.NewCurrency64(500), + FreeSectorPrice: types.NewCurrency64(600), + } + renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() + + type testCase struct { + Description string + Modify func(*types.V2FileContract, *RPCRenewContractParams) + RenterCost types.Currency + HostCost types.Currency + } + + cases := []testCase{ + { + Description: "empty", + Modify: func(*types.V2FileContract, *RPCRenewContractParams) {}, + RenterCost: prices.ContractPrice, + }, + { + Description: "no storage", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + }, + RenterCost: types.Siacoins(20).Add(prices.ContractPrice), + HostCost: types.Siacoins(10), + }, + { + Description: "no storage - no renter rollover", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + // transfer all of the renter funds to the host so the renter will need to put up the entire allowance + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + RenterCost: types.Siacoins(320).Add(prices.ContractPrice), + HostCost: types.Siacoins(10), + }, + { + Description: "renewed storage - no additional funds", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + }, + RenterCost: prices.ContractPrice.Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.ZeroCurrency, // collateral lock up is less than rollover + }, + { + Description: "renewed storage", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + + // adjust the renewal params + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + }, + RenterCost: types.Siacoins(20).Add(prices.ContractPrice).Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.Siacoins(10).Add(prices.Collateral.Mul64(SectorSize).Mul64(renewalDuration)), // collateral is calculated for the full duration + }, + { + Description: "renewed storage - no renter rollover", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // adjust the renewal params + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + // transfer all the renter funds to the host so the renter will need to put up more allowance + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + RenterCost: types.Siacoins(320).Add(prices.ContractPrice).Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.Siacoins(10).Add(prices.Collateral.Mul64(SectorSize).Mul64(renewalDuration)), // collateral is calculated for the full duration + }, + } + for _, tc := range cases { + t.Run(tc.Description, func(t *testing.T) { + contract, _ := NewContract(prices, RPCFormContractParams{ + RenterPublicKey: renterKey, + RenterAddress: types.StandardAddress(renterKey), + Allowance: types.Siacoins(300), + Collateral: types.Siacoins(400), + ProofHeight: initialProofHeight, + }, hostKey, types.StandardAddress(hostKey)) + + params := RPCRenewContractParams{ + ProofHeight: renewalProofHeight, + } + tc.Modify(&contract, ¶ms) + + prices.TipHeight = renewalHeight + renewal, _ := RenewContract(contract, prices, params) + tax := cs.V2FileContractTax(renewal.NewContract) + renter, host := RenewalCost(cs, prices, renewal, types.ZeroCurrency) + if !renter.Equals(tc.RenterCost.Add(tax)) { + t.Errorf("expected renter cost %v, got %v", tc.RenterCost, renter.Sub(tax)) + } else if !host.Equals(tc.HostCost) { + t.Errorf("expected host cost %v, got %v", tc.HostCost, host) + } + }) + } +} + +func TestRefreshCost(t *testing.T) { + const initialProofHeight = 1000 + + cs := consensus.State{} + prices := HostPrices{ + ContractPrice: types.NewCurrency64(100), + Collateral: types.NewCurrency64(200), + StoragePrice: types.NewCurrency64(300), + IngressPrice: types.NewCurrency64(400), + EgressPrice: types.NewCurrency64(500), + FreeSectorPrice: types.NewCurrency64(600), + } + renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() + + type testCase struct { + Description string + Modify func(*types.V2FileContract) + } + + cases := []testCase{ + { + Description: "no storage", + Modify: func(rev *types.V2FileContract) {}, + }, + { + Description: "no storage - no renter rollover", + Modify: func(rev *types.V2FileContract) { + // transfer all of the renter funds to the host so the renter rolls over nothing + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + }, + { + Description: "renewed storage", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + }, + }, + { + Description: "renewed storage - no renter rollover", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + // transfer all the renter funds to the host + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + }, + } + + // the actual cost to the renter and host should always be the additional allowance and collateral + // on top of the existing contract costs + additionalAllowance, additionalCollateral := types.Siacoins(20), types.Siacoins(10) + renterCost := additionalAllowance.Add(prices.ContractPrice) + hostCost := additionalCollateral + + for _, tc := range cases { + t.Run(tc.Description, func(t *testing.T) { + contract, _ := NewContract(prices, RPCFormContractParams{ + RenterPublicKey: renterKey, + RenterAddress: types.StandardAddress(renterKey), + Allowance: types.Siacoins(300), + Collateral: types.Siacoins(400), + ProofHeight: initialProofHeight, + }, hostKey, types.StandardAddress(hostKey)) + + params := RPCRefreshContractParams{ + Allowance: additionalAllowance, + Collateral: additionalCollateral, + } + tc.Modify(&contract) + + refresh, _ := RefreshContract(contract, prices, params) + tax := cs.V2FileContractTax(refresh.NewContract) + renter, host := RefreshCost(cs, prices, refresh, types.ZeroCurrency) + if !renter.Equals(renterCost.Add(tax)) { + t.Errorf("expected renter cost %v, got %v", renterCost, renter.Sub(tax)) + } else if !host.Equals(hostCost) { + t.Errorf("expected host cost %v, got %v", hostCost, host) + } + }) + } +} From 23e9cdeaad697162a8a8331ee4c793ec32ebc47a Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 11 Dec 2024 08:45:35 -0800 Subject: [PATCH 26/45] add miner fee --- rhp/v4/rhp_test.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/rhp/v4/rhp_test.go b/rhp/v4/rhp_test.go index 295094ba..3d552715 100644 --- a/rhp/v4/rhp_test.go +++ b/rhp/v4/rhp_test.go @@ -1,10 +1,12 @@ package rhp import ( + "math" "testing" "go.sia.tech/core/consensus" "go.sia.tech/core/types" + "lukechampine.com/frand" ) func TestMinRenterAllowance(t *testing.T) { @@ -41,6 +43,7 @@ func TestRenewalCost(t *testing.T) { EgressPrice: types.NewCurrency64(500), FreeSectorPrice: types.NewCurrency64(600), } + minerFee := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() type testCase struct { @@ -135,12 +138,18 @@ func TestRenewalCost(t *testing.T) { prices.TipHeight = renewalHeight renewal, _ := RenewContract(contract, prices, params) tax := cs.V2FileContractTax(renewal.NewContract) - renter, host := RenewalCost(cs, prices, renewal, types.ZeroCurrency) - if !renter.Equals(tc.RenterCost.Add(tax)) { - t.Errorf("expected renter cost %v, got %v", tc.RenterCost, renter.Sub(tax)) + renter, host := RenewalCost(cs, prices, renewal, minerFee) + if !renter.Equals(tc.RenterCost.Add(tax).Add(minerFee)) { + t.Errorf("expected renter cost %v, got %v", tc.RenterCost, renter.Sub(tax).Sub(minerFee)) } else if !host.Equals(tc.HostCost) { t.Errorf("expected host cost %v, got %v", tc.HostCost, host) } + + contractTotal := renewal.NewContract.HostOutput.Value.Add(renewal.NewContract.RenterOutput.Value) + totalCost := renter.Add(host).Add(renewal.HostRollover).Add(renewal.RenterRollover).Sub(tax).Sub(minerFee) + if !contractTotal.Equals(totalCost) { + t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + } }) } } @@ -158,6 +167,7 @@ func TestRefreshCost(t *testing.T) { FreeSectorPrice: types.NewCurrency64(600), } renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() + minerFee := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) type testCase struct { Description string @@ -220,12 +230,18 @@ func TestRefreshCost(t *testing.T) { refresh, _ := RefreshContract(contract, prices, params) tax := cs.V2FileContractTax(refresh.NewContract) - renter, host := RefreshCost(cs, prices, refresh, types.ZeroCurrency) - if !renter.Equals(renterCost.Add(tax)) { - t.Errorf("expected renter cost %v, got %v", renterCost, renter.Sub(tax)) + renter, host := RefreshCost(cs, prices, refresh, minerFee) + if !renter.Equals(renterCost.Add(tax).Add(minerFee)) { + t.Errorf("expected renter cost %v, got %v", renterCost, renter.Sub(tax).Sub(minerFee)) } else if !host.Equals(hostCost) { t.Errorf("expected host cost %v, got %v", hostCost, host) } + + contractTotal := refresh.NewContract.HostOutput.Value.Add(refresh.NewContract.RenterOutput.Value) + totalCost := renter.Add(host).Add(refresh.HostRollover).Add(refresh.RenterRollover).Sub(tax).Sub(minerFee) + if !contractTotal.Equals(totalCost) { + t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + } }) } } From 5dc3661c3ee19a423b0462f3fed888e9bf332016 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 11 Dec 2024 08:51:19 -0800 Subject: [PATCH 27/45] rhp4: add cases for capacity --- rhp/v4/rhp_test.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/rhp/v4/rhp_test.go b/rhp/v4/rhp_test.go index 3d552715..0e21b0eb 100644 --- a/rhp/v4/rhp_test.go +++ b/rhp/v4/rhp_test.go @@ -89,6 +89,16 @@ func TestRenewalCost(t *testing.T) { RenterCost: prices.ContractPrice.Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension HostCost: types.ZeroCurrency, // collateral lock up is less than rollover }, + { + Description: "renewed storage - greater capacity", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize * 2 + rev.Filesize = SectorSize + }, + RenterCost: prices.ContractPrice.Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the filesize & extension + HostCost: types.ZeroCurrency, // collateral lock up is less than rollover + }, { Description: "renewed storage", Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { @@ -147,8 +157,13 @@ func TestRenewalCost(t *testing.T) { contractTotal := renewal.NewContract.HostOutput.Value.Add(renewal.NewContract.RenterOutput.Value) totalCost := renter.Add(host).Add(renewal.HostRollover).Add(renewal.RenterRollover).Sub(tax).Sub(minerFee) - if !contractTotal.Equals(totalCost) { + switch { + case !contractTotal.Equals(totalCost): t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + case contract.Filesize != renewal.NewContract.Filesize: + t.Fatalf("expected contract size %d, got %d", contract.Filesize, renewal.NewContract.Filesize) + case contract.Filesize != renewal.NewContract.Capacity: // renewals reset capacity + t.Fatalf("expected contract capacity %d, got %d", contract.Filesize, renewal.NewContract.Capacity) } }) } @@ -194,6 +209,14 @@ func TestRefreshCost(t *testing.T) { rev.Filesize = SectorSize }, }, + { + Description: "renewed storage - greater capacity", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize * 4 + rev.Filesize = SectorSize + }, + }, { Description: "renewed storage - no renter rollover", Modify: func(rev *types.V2FileContract) { @@ -239,8 +262,14 @@ func TestRefreshCost(t *testing.T) { contractTotal := refresh.NewContract.HostOutput.Value.Add(refresh.NewContract.RenterOutput.Value) totalCost := renter.Add(host).Add(refresh.HostRollover).Add(refresh.RenterRollover).Sub(tax).Sub(minerFee) - if !contractTotal.Equals(totalCost) { + + switch { + case !contractTotal.Equals(totalCost): t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + case contract.Filesize != refresh.NewContract.Filesize: + t.Fatalf("expected contract size %d, got %d", contract.Filesize, refresh.NewContract.Filesize) + case contract.Capacity != refresh.NewContract.Capacity: + t.Fatalf("expected contract capacity %d, got %d", contract.Capacity, refresh.NewContract.Capacity) } }) } From 67ca48ea5766ddfdc282a792585aa7dce99ca940 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:14:40 +0000 Subject: [PATCH 28/45] chore: prepare release 0.7.2 --- CHANGELOG.md | 12 ++++++++++++ go.mod | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d1b5f8..621123ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.7.2 (2024-12-12) + +### Features + +#### Allow revisions to set MaxRevisionNumber + +`MaxRevisionNumber` was previously used to finalize contracts, but that is not the case anymore, so the restriction can be removed. + +### Fixes + +- Include storage cost in renter renewal cost + ## 0.7.1 (2024-12-04) ### Fixes diff --git a/go.mod b/go.mod index 1939fa5c..183e234c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core // v0.7.1 +module go.sia.tech/core // v0.7.2 go 1.23.1 From 6fc49ab6a6818bb187492399b63eb34d70dd0185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:21:10 +0000 Subject: [PATCH 29/45] build(deps): bump golang.org/x/crypto in the go_modules group Bumps the go_modules group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.30.0 to 0.31.0 - [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production dependency-group: go_modules ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 183e234c..e9cdbc4b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.2 require ( go.sia.tech/mux v1.3.0 - golang.org/x/crypto v0.30.0 + golang.org/x/crypto v0.31.0 golang.org/x/sys v0.28.0 lukechampine.com/frand v1.5.1 ) diff --git a/go.sum b/go.sum index 95a86e94..812a6b98 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= From 2321b69c836feec6aab9759d3be987d6ed1ab484 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 12 Dec 2024 07:44:35 -0800 Subject: [PATCH 30/45] chore: cleanup old changesets --- .changeset/allow-max-revision.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changeset/allow-max-revision.md diff --git a/.changeset/allow-max-revision.md b/.changeset/allow-max-revision.md deleted file mode 100644 index a9057e02..00000000 --- a/.changeset/allow-max-revision.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: minor ---- - -# Allow revisions to set MaxRevisionNumber - -`MaxRevisionNumber` was previously used to finalize contracts, but that is not the case anymore, so the restriction can be removed. \ No newline at end of file From 109d18fd84d9b9764f4f9cdccb2a10c97a89d518 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 12 Dec 2024 07:51:13 -0800 Subject: [PATCH 31/45] chore: fix missing changelog entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621123ce..28c69236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.7.3 (2024-12-12) + +### Features + +- Update `golang.org/x/crypto` from 0.30.0 to 0.31.0 + ## 0.7.2 (2024-12-12) ### Features From c36fa2aec558283fff556979c776270677978d8f Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 12 Dec 2024 07:52:27 -0800 Subject: [PATCH 32/45] chore: fix go.mod version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e9cdbc4b..c8f029e7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core // v0.7.2 +module go.sia.tech/core // v0.7.3 go 1.23.1 From 0875745a2429bc05b95b47c43a8e824d9431df4a Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 12 Dec 2024 11:11:58 -0800 Subject: [PATCH 33/45] add revisable and renewed to RPCLatestRevision response --- .changeset/add_revisable_to_rpclatestrevision.md | 7 +++++++ rhp/v4/encoding.go | 4 ++++ rhp/v4/rhp.go | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/add_revisable_to_rpclatestrevision.md diff --git a/.changeset/add_revisable_to_rpclatestrevision.md b/.changeset/add_revisable_to_rpclatestrevision.md new file mode 100644 index 00000000..7305ae39 --- /dev/null +++ b/.changeset/add_revisable_to_rpclatestrevision.md @@ -0,0 +1,7 @@ +--- +default: major +--- + +# Add revisable to RPCLatestRevision + +Adds two additional flags to the RPCLatestRevision response. The `Revisable` field indicates whether the host will accept further revisions to the contract. A host will not accept revisions too close to the proof window or revisions on contracts that have already been resolved. The `Renewed` field indicates whether the contract was renewed. If the contract was renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 8172400b..9bcb874b 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -473,9 +473,13 @@ func (r *RPCLatestRevisionRequest) maxLen() int { func (r *RPCLatestRevisionResponse) encodeTo(e *types.Encoder) { r.Contract.EncodeTo(e) + e.WriteBool(r.Revisable) + e.WriteBool(r.Renewed) } func (r *RPCLatestRevisionResponse) decodeFrom(d *types.Decoder) { r.Contract.DecodeFrom(d) + r.Revisable = d.ReadBool() + r.Renewed = d.ReadBool() } func (r *RPCLatestRevisionResponse) maxLen() int { return sizeofContract diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 05d1f164..61fe2106 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -383,7 +383,9 @@ type ( } // RPCLatestRevisionResponse implements Object. RPCLatestRevisionResponse struct { - Contract types.V2FileContract `json:"contract"` + Contract types.V2FileContract `json:"contract"` + Revisable bool `json:"revisable"` + Renewed bool `json:"renewed"` } // RPCReadSectorRequest implements Object. From 8383d656cbed9a0aaa0e09edf1751e44043c2b31 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 12 Dec 2024 11:13:04 -0800 Subject: [PATCH 34/45] improve docstring --- rhp/v4/rhp.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 61fe2106..c7004ccc 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -382,6 +382,12 @@ type ( ContractID types.FileContractID `json:"contractID"` } // RPCLatestRevisionResponse implements Object. + // + // The `Revisable` field indicates whether the + // host will accept further revisions to the contract. A host will not accept revisions too + // close to the proof window or revisions on contracts that have already been resolved. + // The `Renewed` field indicates whether the contract was renewed. If the contract was + // renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. RPCLatestRevisionResponse struct { Contract types.V2FileContract `json:"contract"` Revisable bool `json:"revisable"` From 35e29c01aecc92f6d332a58a25020e27e17f9bf7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:05:43 +0000 Subject: [PATCH 35/45] chore: prepare release 0.8.0 --- .changeset/add_revisable_to_rpclatestrevision.md | 7 ------- CHANGELOG.md | 8 ++++++++ go.mod | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/add_revisable_to_rpclatestrevision.md diff --git a/.changeset/add_revisable_to_rpclatestrevision.md b/.changeset/add_revisable_to_rpclatestrevision.md deleted file mode 100644 index 7305ae39..00000000 --- a/.changeset/add_revisable_to_rpclatestrevision.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -default: major ---- - -# Add revisable to RPCLatestRevision - -Adds two additional flags to the RPCLatestRevision response. The `Revisable` field indicates whether the host will accept further revisions to the contract. A host will not accept revisions too close to the proof window or revisions on contracts that have already been resolved. The `Renewed` field indicates whether the contract was renewed. If the contract was renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c69236..6390a2e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.8.0 (2024-12-13) + +### Breaking Changes + +#### Add revisable to RPCLatestRevision + +Adds two additional flags to the RPCLatestRevision response. The `Revisable` field indicates whether the host will accept further revisions to the contract. A host will not accept revisions too close to the proof window or revisions on contracts that have already been resolved. The `Renewed` field indicates whether the contract was renewed. If the contract was renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. + ## 0.7.3 (2024-12-12) ### Features diff --git a/go.mod b/go.mod index c8f029e7..1e3de03b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core // v0.7.3 +module go.sia.tech/core // v0.8.0 go 1.23.1 From bbb96e62e2696659b3babd8bd8c6711cde98b0f3 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 16 Dec 2024 11:08:46 -0800 Subject: [PATCH 36/45] rhp4: add host key to account token --- .../add_host_public_key_to_accounttoken.md | 5 ++ rhp/v4/encoding.go | 2 + rhp/v4/encoding_test.go | 49 +++++++++++++++++++ rhp/v4/rhp.go | 25 +--------- rhp/v4/validation.go | 45 +++++++++++++---- rhp/v4/validation_test.go | 43 ++++++++++++++++ 6 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 .changeset/add_host_public_key_to_accounttoken.md create mode 100644 rhp/v4/encoding_test.go create mode 100644 rhp/v4/validation_test.go diff --git a/.changeset/add_host_public_key_to_accounttoken.md b/.changeset/add_host_public_key_to_accounttoken.md new file mode 100644 index 00000000..ad58d59f --- /dev/null +++ b/.changeset/add_host_public_key_to_accounttoken.md @@ -0,0 +1,5 @@ +--- +default: major +--- + +# Add host public key to AccountToken \ No newline at end of file diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 9bcb874b..6205da50 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -77,12 +77,14 @@ func (a Account) EncodeTo(e *types.Encoder) { e.Write(a[:]) } func (a *Account) DecodeFrom(d *types.Decoder) { d.Read(a[:]) } func (at AccountToken) encodeTo(e *types.Encoder) { + at.HostKey.EncodeTo(e) at.Account.EncodeTo(e) e.WriteTime(at.ValidUntil) at.Signature.EncodeTo(e) } func (at *AccountToken) decodeFrom(d *types.Decoder) { + at.HostKey.DecodeFrom(d) at.Account.DecodeFrom(d) at.ValidUntil = d.ReadTime() at.Signature.DecodeFrom(d) diff --git a/rhp/v4/encoding_test.go b/rhp/v4/encoding_test.go new file mode 100644 index 00000000..124b00f8 --- /dev/null +++ b/rhp/v4/encoding_test.go @@ -0,0 +1,49 @@ +package rhp + +import ( + "bytes" + "math" + "reflect" + "testing" + "time" + + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +type rhpEncodable[T any] interface { + *T + encodeTo(*types.Encoder) + decodeFrom(*types.Decoder) +} + +func testRoundtrip[T any, PT rhpEncodable[T]](a PT) func(t *testing.T) { + return func(t *testing.T) { + buf := bytes.NewBuffer(nil) + enc := types.NewEncoder(buf) + + a.encodeTo(enc) + if err := enc.Flush(); err != nil { + t.Fatal(err) + } + + b := new(T) + dec := types.NewBufDecoder(buf.Bytes()) + PT(b).decodeFrom(dec) + + if !reflect.DeepEqual(a, b) { + t.Log(a) + t.Log(reflect.ValueOf(b).Elem()) + t.Fatal("expected rountrip to match") + } + } +} + +func TestEncodingRoundtrip(t *testing.T) { + t.Run("AccountToken", testRoundtrip(&AccountToken{ + HostKey: frand.Entropy256(), + Account: frand.Entropy256(), + ValidUntil: time.Unix(int64(frand.Intn(math.MaxInt)), 0), + Signature: types.Signature(frand.Bytes(64)), + })) +} diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index c7004ccc..2fbcd804 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -160,18 +160,6 @@ func (hp HostPrices) SigHash() types.Hash256 { return h.Sum() } -// Validate checks the host prices for validity. It returns an error if the -// prices have expired or the signature is invalid. -func (hp *HostPrices) Validate(pk types.PublicKey) error { - if time.Until(hp.ValidUntil) <= 0 { - return ErrPricesExpired - } - if !pk.VerifyHash(hp.SigHash(), hp.Signature) { - return ErrInvalidSignature - } - return nil -} - // HostSettings specify the settings of a host. type HostSettings struct { ProtocolVersion [3]uint8 `json:"protocolVersion"` @@ -208,6 +196,7 @@ func (a *Account) UnmarshalText(b []byte) error { // An AccountToken authorizes an account action. type AccountToken struct { + HostKey types.PublicKey `json:"hostKey"` Account Account `json:"account"` ValidUntil time.Time `json:"validUntil"` Signature types.Signature `json:"signature"` @@ -216,22 +205,12 @@ type AccountToken struct { // SigHash returns the hash of the account token used for signing. func (at *AccountToken) SigHash() types.Hash256 { h := types.NewHasher() + at.HostKey.EncodeTo(h.E) at.Account.EncodeTo(h.E) h.E.WriteTime(at.ValidUntil) return h.Sum() } -// Validate verifies the account token is valid for use. It returns an error if -// the token has expired or the signature is invalid. -func (at AccountToken) Validate() error { - if time.Now().After(at.ValidUntil) { - return NewRPCError(ErrorCodeBadRequest, "account token expired") - } else if !types.PublicKey(at.Account).VerifyHash(at.SigHash(), at.Signature) { - return ErrInvalidSignature - } - return nil -} - // GenerateAccount generates a pair of private key and Account from a secure // entropy source. func GenerateAccount() (types.PrivateKey, Account) { diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go index 9ca1dc5e..1896195f 100644 --- a/rhp/v4/validation.go +++ b/rhp/v4/validation.go @@ -3,15 +3,42 @@ package rhp import ( "errors" "fmt" + "time" "go.sia.tech/core/types" ) +// Validate checks the host prices for validity. It returns an error if the +// prices have expired or the signature is invalid. +func (hp *HostPrices) Validate(pk types.PublicKey) error { + if time.Until(hp.ValidUntil) <= 0 { + return ErrPricesExpired + } + if !pk.VerifyHash(hp.SigHash(), hp.Signature) { + return ErrInvalidSignature + } + return nil +} + +// Validate verifies the account token is valid for use. It returns an error if +// the token has expired or the signature is invalid. +func (at AccountToken) Validate(hostKey types.PublicKey) error { + switch { + case at.HostKey != hostKey: + return NewRPCError(ErrorCodeBadRequest, "host key mismatch") + case time.Now().After(at.ValidUntil): + return NewRPCError(ErrorCodeBadRequest, "account token expired") + case !types.PublicKey(at.Account).VerifyHash(at.SigHash(), at.Signature): + return ErrInvalidSignature + } + return nil +} + // Validate validates a read sector request. -func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCReadSectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); err != nil { return fmt.Errorf("token is invalid: %w", err) } switch { @@ -26,10 +53,10 @@ func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { } // Validate validates a write sector request. -func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCWriteSectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); err != nil { return fmt.Errorf("token is invalid: %w", err) } switch { @@ -200,10 +227,10 @@ func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, existingTotal } // Validate checks that the request is valid -func (req *RPCVerifySectorRequest) Validate(pk types.PublicKey) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCVerifySectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); err != nil { return fmt.Errorf("token is invalid: %w", err) } else if req.LeafIndex >= LeavesPerSector { return fmt.Errorf("leaf index must be less than %d", LeavesPerSector) diff --git a/rhp/v4/validation_test.go b/rhp/v4/validation_test.go new file mode 100644 index 00000000..77cb1878 --- /dev/null +++ b/rhp/v4/validation_test.go @@ -0,0 +1,43 @@ +package rhp + +import ( + "errors" + "strings" + "testing" + "time" + + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +func TestValidateAccountToken(t *testing.T) { + hostKey := types.GeneratePrivateKey().PublicKey() + renterKey := types.GeneratePrivateKey() + account := Account(renterKey.PublicKey()) + + ac := AccountToken{ + HostKey: hostKey, + Account: account, + ValidUntil: time.Now(), + } + + if err := ac.Validate(frand.Entropy256()); !strings.Contains(err.Error(), "host key mismatch") { + t.Fatalf("expected host key mismatch, got %v", err) + } + + if err := ac.Validate(hostKey); !strings.Contains(err.Error(), "token expired") { + t.Fatalf("expected token expired, got %v", err) + } + + ac.ValidUntil = time.Now().Add(time.Minute) + + if err := ac.Validate(hostKey); !errors.Is(err, ErrInvalidSignature) { + t.Fatalf("expected ErrInvalidSignature, got %v", err) + } + + ac.Signature = renterKey.SignHash(ac.SigHash()) + + if err := ac.Validate(hostKey); err != nil { + t.Fatal(err) + } +} From fe0cae1b916b0a124c7577e2c056d6a8dae3590d Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 16 Dec 2024 11:25:13 -0800 Subject: [PATCH 37/45] rhp4: fix flaky validate test --- rhp/v4/validation_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/rhp/v4/validation_test.go b/rhp/v4/validation_test.go index 77cb1878..2bb548cd 100644 --- a/rhp/v4/validation_test.go +++ b/rhp/v4/validation_test.go @@ -18,19 +18,16 @@ func TestValidateAccountToken(t *testing.T) { ac := AccountToken{ HostKey: hostKey, Account: account, - ValidUntil: time.Now(), + ValidUntil: time.Now().Add(-time.Minute), } if err := ac.Validate(frand.Entropy256()); !strings.Contains(err.Error(), "host key mismatch") { t.Fatalf("expected host key mismatch, got %v", err) - } - - if err := ac.Validate(hostKey); !strings.Contains(err.Error(), "token expired") { + } else if err := ac.Validate(hostKey); !strings.Contains(err.Error(), "token expired") { t.Fatalf("expected token expired, got %v", err) } ac.ValidUntil = time.Now().Add(time.Minute) - if err := ac.Validate(hostKey); !errors.Is(err, ErrInvalidSignature) { t.Fatalf("expected ErrInvalidSignature, got %v", err) } From 27dae155881b0147b1183946f4ed383e0231e373 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 17 Dec 2024 06:59:32 -0800 Subject: [PATCH 38/45] rhp4: add token helper --- .../add_helper_for_generating_account_tokens.md | 5 +++++ rhp/v4/rhp.go | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .changeset/add_helper_for_generating_account_tokens.md diff --git a/.changeset/add_helper_for_generating_account_tokens.md b/.changeset/add_helper_for_generating_account_tokens.md new file mode 100644 index 00000000..404b749f --- /dev/null +++ b/.changeset/add_helper_for_generating_account_tokens.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +# Add helper for generating account tokens diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 2fbcd804..dc4dd102 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -194,6 +194,18 @@ func (a *Account) UnmarshalText(b []byte) error { return nil } +// Token returns a signed account token authorizing spending from the account on the +// host. +func (a *Account) Token(renterKey types.PrivateKey, hostKey types.PublicKey) AccountToken { + token := AccountToken{ + HostKey: hostKey, + Account: Account(renterKey.PublicKey()), + ValidUntil: time.Now().Add(5 * time.Minute), + } + token.Signature = renterKey.SignHash(token.SigHash()) + return token +} + // An AccountToken authorizes an account action. type AccountToken struct { HostKey types.PublicKey `json:"hostKey"` From 5a107eb487a83a73a11a3619199b0024dd3b6d91 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 16 Dec 2024 15:32:28 -0500 Subject: [PATCH 39/45] consensus: Allow v1 contracts to be resolved immediately --- consensus/application.go | 2 +- consensus/state.go | 7 + consensus/validation.go | 31 ++-- consensus/validation_test.go | 289 +++++++++++++++++++---------------- 4 files changed, 182 insertions(+), 147 deletions(-) diff --git a/consensus/application.go b/consensus/application.go index 22e23a58..037845be 100644 --- a/consensus/application.go +++ b/consensus/application.go @@ -495,7 +495,7 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp ms.reviseFileContractElement(fce, fcr.FileContract) } for _, sp := range txn.StorageProofs { - sps, ok := ts.storageProof(sp.ParentID) + sps, ok := ms.storageProof(ts, sp.ParentID) if !ok { panic("missing V1StorageProofSupplement") } diff --git a/consensus/state.go b/consensus/state.go index 7b45225d..f5c5c370 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -630,6 +630,13 @@ func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.Fil return ts.revision(id) } +func (ms *MidState) storageProof(ts V1TransactionSupplement, id types.FileContractID) (V1StorageProofSupplement, bool) { + if i, ok := ms.created[id]; ok && ms.fces[i].FileContract.WindowStart == ms.base.childHeight() { + return V1StorageProofSupplement{FileContract: ms.fces[i], WindowID: ms.base.Index.ID}, true + } + return ts.storageProof(id) +} + func (ms *MidState) spent(id types.ElementID) (types.TransactionID, bool) { txid, ok := ms.spends[id] return txid, ok diff --git a/consensus/validation.go b/consensus/validation.go index ee5e80dd..202ea1bc 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -15,9 +15,9 @@ func validateHeader(s State, parentID types.BlockID, timestamp time.Time, nonce if parentID != s.Index.ID { return errors.New("wrong parent ID") } else if timestamp.Before(s.medianTimestamp()) { - return errors.New("timestamp is too far in the past") + return errors.New("timestamp too far in the past") } else if nonce%s.NonceFactor() != 0 { - return errors.New("nonce is not divisible by required factor") + return errors.New("nonce not divisible by required factor") } else if id.CmpWork(s.ChildTarget) < 0 { return errors.New("insufficient work") } @@ -71,7 +71,7 @@ func validateMinerPayouts(s State, b types.Block) error { } } if sum != expectedSum { - return fmt.Errorf("miner payout sum (%d) does not match block reward + fees (%d)", sum, expectedSum) + return fmt.Errorf("miner payout sum (%v) does not match block reward + fees (%v)", sum, expectedSum) } return nil } @@ -90,7 +90,7 @@ func ValidateOrphan(s State, b types.Block) error { } else if err := validateMinerPayouts(s, b); err != nil { return err } else if err := validateHeader(s, b.ParentID, b.Timestamp, b.Nonce, b.ID()); err != nil { - return err + return fmt.Errorf("block has %v", err) } if b.V2 != nil { if b.V2.Height != s.Index.Height+1 { @@ -202,7 +202,7 @@ func validateSiacoins(ms *MidState, txn types.Transaction, ts V1TransactionSuppl outputSum = outputSum.Add(fee) } if inputSum.Cmp(outputSum) != 0 { - return fmt.Errorf("siacoin inputs (%d H) do not equal outputs (%d H)", inputSum, outputSum) + return fmt.Errorf("siacoin inputs (%v) do not equal outputs (%v)", inputSum, outputSum) } return nil } @@ -351,14 +351,17 @@ func validateFileContracts(ms *MidState, txn types.Transaction, ts V1Transaction } for i, sp := range txn.StorageProofs { - if txid, ok := ms.spent(types.Hash256(sp.ParentID)); ok { + if txid, ok := ms.spent(sp.ParentID); ok { return fmt.Errorf("storage proof %v conflicts with previous proof (in %v)", i, txid) } - sps, ok := ts.storageProof(sp.ParentID) + sps, ok := ms.storageProof(ts, sp.ParentID) if !ok { return fmt.Errorf("storage proof %v references nonexistent file contract", i) } fc := sps.FileContract.FileContract + if ms.base.childHeight() < fc.WindowStart { + return fmt.Errorf("storage proof %v cannot be submitted until after window start (%v)", i, fc.WindowStart) + } leafIndex := ms.base.StorageProofLeafIndex(fc.Filesize, sps.WindowID, sp.ParentID) leaf := storageProofLeaf(leafIndex, fc.Filesize, sp.Leaf) if leaf == nil { @@ -617,7 +620,7 @@ func validateV2Siacoins(ms *MidState, txn types.V2Transaction) error { } outputSum = outputSum.Add(txn.MinerFee) if inputSum != outputSum { - return fmt.Errorf("siacoin inputs (%d H) do not equal outputs (%d H)", inputSum, outputSum) + return fmt.Errorf("siacoin inputs (%v) do not equal outputs (%v)", inputSum, outputSum) } return nil @@ -711,9 +714,9 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case fc.RenterOutput.Value.IsZero() && fc.HostOutput.Value.IsZero(): return fmt.Errorf("has zero value") case fc.MissedHostValue.Cmp(fc.HostOutput.Value) > 0: - return fmt.Errorf("has missed host value (%d H) exceeding valid host value (%d H)", fc.MissedHostValue, fc.HostOutput.Value) + return fmt.Errorf("has missed host value (%v) exceeding valid host value (%v)", fc.MissedHostValue, fc.HostOutput.Value) case fc.TotalCollateral.Cmp(fc.HostOutput.Value) > 0: - return fmt.Errorf("has total collateral (%d H) exceeding valid host value (%d H)", fc.TotalCollateral, fc.HostOutput.Value) + return fmt.Errorf("has total collateral (%v) exceeding valid host value (%v)", fc.TotalCollateral, fc.HostOutput.Value) } return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey) } @@ -735,9 +738,9 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case rev.RevisionNumber <= cur.RevisionNumber: return fmt.Errorf("does not increase revision number (%v -> %v)", cur.RevisionNumber, rev.RevisionNumber) case !revOutputSum.Equals(curOutputSum): - return fmt.Errorf("modifies output sum (%d H -> %d H)", curOutputSum, revOutputSum) + return fmt.Errorf("modifies output sum (%v -> %v)", curOutputSum, revOutputSum) case rev.MissedHostValue.Cmp(cur.MissedHostValue) > 0: - return fmt.Errorf("has missed host value (%d H) exceeding old value (%d H)", rev.MissedHostValue, cur.MissedHostValue) + return fmt.Errorf("has missed host value (%v) exceeding old value (%v)", rev.MissedHostValue, cur.MissedHostValue) case rev.TotalCollateral != cur.TotalCollateral: return errors.New("modifies total collateral") case rev.ProofHeight < ms.base.childHeight(): @@ -789,12 +792,12 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { Add(renewal.FinalHostOutput.Value).Add(renewal.HostRollover) existingPayout := fc.RenterOutput.Value.Add(fc.HostOutput.Value) if totalPayout != existingPayout { - return fmt.Errorf("file contract renewal %d renewal payout (%s) does not match existing contract payout %s", i, totalPayout, existingPayout) + return fmt.Errorf("file contract renewal %v renewal payout (%v) does not match existing contract payout %v", i, totalPayout, existingPayout) } newContractCost := renewal.NewContract.RenterOutput.Value.Add(renewal.NewContract.HostOutput.Value).Add(ms.base.V2FileContractTax(renewal.NewContract)) if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 { - return fmt.Errorf("file contract renewal %v has rollover (%d H) exceeding new contract cost (%d H)", i, rollover, newContractCost) + return fmt.Errorf("file contract renewal %v has rollover (%v) exceeding new contract cost (%v)", i, rollover, newContractCost) } else if err := validateContract(renewal.NewContract); err != nil { return fmt.Errorf("file contract renewal %v initial revision %s", i, err) } diff --git a/consensus/validation_test.go b/consensus/validation_test.go index c4cbf93e..03c36cf4 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -3,6 +3,7 @@ package consensus import ( "bytes" "errors" + "fmt" "math" "math/bits" "strings" @@ -42,10 +43,11 @@ func testnet() (*Network, types.Block) { } type consensusDB struct { - sces map[types.SiacoinOutputID]types.SiacoinElement - sfes map[types.SiafundOutputID]types.SiafundElement - fces map[types.FileContractID]types.FileContractElement - v2fces map[types.FileContractID]types.V2FileContractElement + sces map[types.SiacoinOutputID]types.SiacoinElement + sfes map[types.SiafundOutputID]types.SiafundElement + fces map[types.FileContractID]types.FileContractElement + v2fces map[types.FileContractID]types.V2FileContractElement + blockIDs []types.BlockID } func (db *consensusDB) applyBlock(au ApplyUpdate) { @@ -97,6 +99,7 @@ func (db *consensusDB) applyBlock(au ApplyUpdate) { delete(db.v2fces, types.FileContractID(fce.ID)) } }) + db.blockIDs = append(db.blockIDs, au.ms.cie.ID) } func (db *consensusDB) revertBlock(ru RevertUpdate) { @@ -171,6 +174,14 @@ func (db *consensusDB) supplementTipBlock(b types.Block) (bs V1BlockSupplement) ts.RevisedFileContracts = append(ts.RevisedFileContracts, fce) } } + for _, sp := range txn.StorageProofs { + if fce, ok := db.fces[sp.ParentID]; ok { + ts.StorageProofs = append(ts.StorageProofs, V1StorageProofSupplement{ + FileContract: fce, + WindowID: db.blockIDs[fce.FileContract.WindowStart], + }) + } + } } return bs } @@ -324,40 +335,48 @@ func TestValidateBlock(t *testing.T) { b := types.Block{ ParentID: genesisBlock.ID(), Timestamp: types.CurrentTimestamp(), - Transactions: []types.Transaction{{ - SiacoinInputs: []types.SiacoinInput{{ - ParentID: giftTxn.SiacoinOutputID(0), - UnlockConditions: types.StandardUnlockConditions(giftPublicKey), - }}, - SiafundInputs: []types.SiafundInput{{ - ParentID: giftTxn.SiafundOutputID(0), - ClaimAddress: types.VoidAddress, - UnlockConditions: types.StandardUnlockConditions(giftPublicKey), - }}, - SiacoinOutputs: []types.SiacoinOutput{ - {Value: giftAmountSC.Sub(fc.Payout), Address: giftAddress}, - }, - SiafundOutputs: []types.SiafundOutput{ - {Value: giftAmountSF / 2, Address: giftAddress}, - {Value: giftAmountSF / 2, Address: types.VoidAddress}, - }, - FileContracts: []types.FileContract{fc}, - FileContractRevisions: []types.FileContractRevision{ - { - ParentID: giftTxn.FileContractID(0), - UnlockConditions: types.UnlockConditions{ - PublicKeys: []types.UnlockKey{renterPublicKey.UnlockKey(), hostPublicKey.UnlockKey()}, - SignaturesRequired: 2, + Transactions: []types.Transaction{ + { + SiacoinInputs: []types.SiacoinInput{{ + ParentID: giftTxn.SiacoinOutputID(0), + UnlockConditions: types.StandardUnlockConditions(giftPublicKey), + }}, + SiafundInputs: []types.SiafundInput{{ + ParentID: giftTxn.SiafundOutputID(0), + ClaimAddress: types.VoidAddress, + UnlockConditions: types.StandardUnlockConditions(giftPublicKey), + }}, + SiacoinOutputs: []types.SiacoinOutput{ + {Value: giftAmountSC.Sub(fc.Payout), Address: giftAddress}, + }, + SiafundOutputs: []types.SiafundOutput{ + {Value: giftAmountSF / 2, Address: giftAddress}, + {Value: giftAmountSF / 2, Address: types.VoidAddress}, + }, + FileContracts: []types.FileContract{fc}, + FileContractRevisions: []types.FileContractRevision{ + { + ParentID: giftTxn.FileContractID(0), + UnlockConditions: types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterPublicKey.UnlockKey(), hostPublicKey.UnlockKey()}, + SignaturesRequired: 2, + }, + FileContract: revision, }, - FileContract: revision, }, }, - }}, + }, MinerPayouts: []types.SiacoinOutput{{ Address: types.VoidAddress, Value: cs.BlockReward(), }}, } + b.Transactions[0].FileContracts[0].FileMerkleRoot = types.HashBytes(make([]byte, 65)) + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: []types.StorageProof{{ + ParentID: b.Transactions[0].FileContractID(0), + }}, + }) // block should be valid validBlock := deepCopyBlock(b) @@ -373,11 +392,11 @@ func TestValidateBlock(t *testing.T) { // tests with correct signatures { tests := []struct { - desc string - corrupt func(*types.Block) + errString string + corrupt func(*types.Block) }{ { - "weight that exceeds the limit", + "block exceeds maximum weight", func(b *types.Block) { data := make([]byte, cs.MaxBlockWeight()) b.Transactions = append(b.Transactions, types.Transaction{ @@ -386,25 +405,25 @@ func TestValidateBlock(t *testing.T) { }, }, { - "wrong parent ID", + "block has wrong parent ID", func(b *types.Block) { b.ParentID[0] ^= 255 }, }, { - "wrong timestamp", + "block has timestamp too far in the past", func(b *types.Block) { b.Timestamp = cs.PrevTimestamps[0].AddDate(-1, 0, 0) }, }, { - "no miner payout", + "miner payout sum (0 SC) does not match block reward + fees (300 KS)", func(b *types.Block) { b.MinerPayouts = nil }, }, { - "zero miner payout", + "miner payout has zero value", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -413,7 +432,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "incorrect miner payout", + "miner payout sum (150 KS) does not match block reward + fees (300 KS)", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -422,7 +441,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "overflowing miner payout", + "miner payouts overflow", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{ {Address: types.VoidAddress, Value: types.MaxCurrency}, @@ -431,7 +450,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "overflowing siacoin outputs", + "transaction outputs exceed inputs", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs = []types.SiacoinOutput{ @@ -441,7 +460,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued SiacoinOutput", + "transaction creates a zero-valued output", func(b *types.Block) { txn := &b.Transactions[0] for i := range txn.SiacoinOutputs { @@ -452,7 +471,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued SiafundOutput", + "transaction creates a zero-valued output", func(b *types.Block) { txn := &b.Transactions[0] for i := range txn.SiafundOutputs { @@ -462,14 +481,14 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued MinerFee", + "transaction fee has zero value", func(b *types.Block) { txn := &b.Transactions[0] txn.MinerFees = append(txn.MinerFees, types.ZeroCurrency) }, }, { - "overflowing MinerFees", + "transaction fees overflow", func(b *types.Block) { txn := &b.Transactions[0] txn.MinerFees = append(txn.MinerFees, types.MaxCurrency) @@ -477,56 +496,58 @@ func TestValidateBlock(t *testing.T) { }, }, { - "siacoin outputs exceed inputs", + "siacoin inputs (100 SC) do not equal outputs (100.000000000000000000000001 SC)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.NewCurrency64(1)) }, }, { - "siacoin outputs less than inputs", + "siacoin inputs (100 SC) do not equal outputs (99.999999999999999999999999 SC)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Sub(types.NewCurrency64(1)) }, }, { - "siafund outputs exceed inputs", + "siafund inputs (100) do not equal outputs (101)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundOutputs[0].Value++ }, }, { - "siafund outputs less than inputs", + "siafund inputs (100) do not equal outputs (99)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundOutputs[0].Value-- }, }, { - "two of the same siacoin input", + fmt.Sprintf("transaction spends siacoin input %v more than once", giftTxn.SiacoinOutputID(0)), func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) + txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(giftAmountSC) }, }, { - "two of the same siafund input", + fmt.Sprintf("transaction spends siafund input %v more than once", giftTxn.SiafundOutputID(0)), func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) + txn.SiafundOutputs[0].Value += giftAmountSF }, }, { - "siacoin input claiming incorrect unlock conditions", + "siacoin input 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 }, }, { - "siafund input claiming incorrect unlock conditions", + "siafund input 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundInputs[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 @@ -565,12 +586,31 @@ func TestValidateBlock(t *testing.T) { }, }, { - "window that starts in the past", + "file contract 0 has window that starts in the past", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContracts[0].WindowStart = 0 }, }, + { + "storage proof 0 references nonexistent file contract", + func(b *types.Block) { + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: []types.StorageProof{{}}, + }) + }, + }, + { + "storage proof 0 conflicts with previous proof", + func(b *types.Block) { + txn := &b.Transactions[0] + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: []types.StorageProof{{ + ParentID: txn.FileContractID(0), + }}, + }) + }, + }, { "window that ends before it begins", func(b *types.Block) { @@ -586,7 +626,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "incorrect payout tax", + "payout with incorrect tax", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(1)) @@ -594,42 +634,43 @@ func TestValidateBlock(t *testing.T) { }, }, { - "revision of nonexistent file contract", + "revises nonexistent file contract", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].ParentID[0] ^= 255 }, }, { - "revision with window that starts in past", + "file contract revision 0 has window that starts in the past", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].WindowStart = cs.Index.Height }, }, { - "revision with window that ends before it begins", + "file contract revision 0 has window that ends before it begins", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].WindowStart = txn.FileContractRevisions[0].WindowEnd }, }, { - "revision with lower revision number than its parent", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].RevisionNumber = 0 + b.Transactions = b.Transactions[:1] }, }, { - "revision claiming incorrect unlock conditions", + "file contract revision 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 }, }, { - "revision having different valid payout sum", + "file contract revision 0 changes valid payout sum", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].ValidProofOutputs = append(txn.FileContractRevisions[0].ValidProofOutputs, types.SiacoinOutput{ @@ -638,7 +679,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "revision having different missed payout sum", + "file contract revision 0 changes missed payout sum", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].MissedProofOutputs = append(txn.FileContractRevisions[0].MissedProofOutputs, types.SiacoinOutput{ @@ -647,7 +688,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "conflicting revisions in same transaction", + fmt.Sprintf("transaction revises file contract %v more than once", giftTxn.FileContractID(0)), func(b *types.Block) { txn := &b.Transactions[0] newRevision := txn.FileContractRevisions[0] @@ -656,12 +697,12 @@ func TestValidateBlock(t *testing.T) { }, }, { - "misordered revisions", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { newRevision := b.Transactions[0].FileContractRevisions[0] newRevision.RevisionNumber = 99 - b.Transactions = append(b.Transactions, types.Transaction{ + b.Transactions = append(b.Transactions[:1], types.Transaction{ FileContractRevisions: []types.FileContractRevision{newRevision}, }) @@ -671,34 +712,18 @@ func TestValidateBlock(t *testing.T) { }, }, { - "duplicate revisions in same block", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { txn := &b.Transactions[0] newRevision := txn.FileContractRevisions[0] - b.Transactions = append(b.Transactions, types.Transaction{ + b.Transactions = append(b.Transactions[:1], types.Transaction{ FileContractRevisions: []types.FileContractRevision{newRevision}, }) }, }, { - "double-spent siacoin input", - func(b *types.Block) { - txn := &b.Transactions[0] - txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) - txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(100)) - }, - }, - { - "double-spent siafund input", - func(b *types.Block) { - txn := &b.Transactions[0] - txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) - txn.SiafundOutputs[0].Value += 100 - }, - }, - { - "transaction contains a storage proof and creates new outputs", + "transaction contains both a storage proof and other outputs", func(b *types.Block) { txn := &b.Transactions[0] txn.StorageProofs = append(txn.StorageProofs, types.StorageProof{}) @@ -713,8 +738,8 @@ func TestValidateBlock(t *testing.T) { } findBlockNonce(cs, &corruptBlock) - if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil { - t.Fatalf("accepted block with %v", test.desc) + if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("expected error containing %q, got %v", test.errString, err) } } } @@ -963,23 +988,23 @@ func TestValidateV2Block(t *testing.T) { { tests := []struct { - desc string - corrupt func(*types.Block) + errString string + corrupt func(*types.Block) }{ { - "v1 transaction after v2 hardfork", + "v1 transactions are not allowed after v2 hardfork", func(b *types.Block) { b.Transactions = []types.Transaction{{}} }, }, { - "block height that does not increment parent height", + "block height does not increment parent height", func(b *types.Block) { b.V2.Height = 0 }, }, { - "weight that exceeds the limit", + "block exceeds maximum weight", func(b *types.Block) { data := make([]byte, cs.MaxBlockWeight()) b.V2.Transactions = append(b.V2.Transactions, types.V2Transaction{ @@ -988,7 +1013,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "empty v2 transaction", + "transactions cannot be empty", func(b *types.Block) { b.V2.Transactions = append(b.V2.Transactions, types.V2Transaction{}) }, @@ -1000,19 +1025,19 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "wrong timestamp", + "block has timestamp too far in the past", func(b *types.Block) { b.Timestamp = cs.PrevTimestamps[0].AddDate(-1, 0, 0) }, }, { - "no miner payout", + "must have exactly one miner payout", func(b *types.Block) { b.MinerPayouts = nil }, }, { - "zero miner payout", + "miner payout has zero value", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -1021,7 +1046,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "incorrect miner payout", + "miner payout sum (150 KS) does not match block reward + fees (300.001 KS)", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -1030,7 +1055,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued SiacoinOutput", + "siacoin output 0 has zero value", func(b *types.Block) { txn := &b.V2.Transactions[0] for i := range txn.SiacoinOutputs { @@ -1041,7 +1066,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued SiafundOutput", + "siafund output 0 has zero value", func(b *types.Block) { txn := &b.V2.Transactions[0] for i := range txn.SiafundOutputs { @@ -1051,77 +1076,77 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued MinerFee", + "miner payout sum (300.001 KS) does not match block reward + fees (300 KS)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.MinerFee = types.ZeroCurrency }, }, { - "overflowing MinerFees", + "v2 transaction fees overflow", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.MinerFee = types.MaxCurrency }, }, { - "siacoin outputs exceed inputs", + "siacoin inputs (100 SC) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.NewCurrency64(1)) }, }, { - "siacoin outputs less than inputs", + "siacoin inputs (100 SC) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Sub(types.NewCurrency64(1)) }, }, { - "siafund outputs exceed inputs", + "siafund inputs (100 SF) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundOutputs[0].Value++ }, }, { - "siafund outputs less than inputs", + "siafund inputs (100 SF) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundOutputs[0].Value-- }, }, { - "two of the same siacoin input", + "siacoin input 1 double-spends parent output", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) }, }, { - "two of the same siafund input", + "siafund input 1 double-spends parent output", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) }, }, { - "siacoin input claiming incorrect policy", + "siacoin input 0 claims incorrect policy", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs[0].SatisfiedPolicy.Policy = types.AnyoneCanSpend() }, }, { - "siafund input claiming incorrect policy", + "siafund input 0 claims incorrect policy", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs[0].SatisfiedPolicy.Policy = types.AnyoneCanSpend() }, }, { - "invalid FoundationAddressUpdate", + "transaction changes Foundation address, but does not spend an input controlled by current address", func(b *types.Block) { txn := &b.V2.Transactions[0] addr := types.VoidAddress @@ -1129,28 +1154,28 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "revision with window that starts in past", + "file contract revision 0 has proof height (0) that has already passed", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.ProofHeight = cs.Index.Height }, }, { - "revision with window that ends before it begins", + "file contract revision 0 leaves no time between proof height (20) and expiration height (20)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.ExpirationHeight = txn.FileContractRevisions[0].Revision.ProofHeight }, }, { - "revision with lower revision number than its parent", + "file contract revision 0 does not increase revision number (0 -> 0)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.RevisionNumber = 0 }, }, { - "misordered revisions", + "file contract revision 0 does not increase revision number (100 -> 99)", func(b *types.Block) { // create a revision b.V2.Transactions[0].FileContractRevisions[0].Revision.RevisionNumber = 100 @@ -1168,14 +1193,14 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "revision having different valid payout sum", + "file contract revision 0 modifies output sum (2 SC -> 3 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.HostOutput.Value = txn.FileContractRevisions[0].Revision.HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "conflicting revisions in same transaction", + fmt.Sprintf("file contract revision 1 parent (%v) has already been revised", fces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] newRevision := txn.FileContractRevisions[0] @@ -1184,28 +1209,28 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "window that starts in the past", + "file contract 0 has proof height (0) that has already passed", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].ProofHeight = 0 }, }, { - "window that ends before it begins", + "file contract 0 leaves no time between proof height (30) and expiration height (30)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].ProofHeight = txn.FileContracts[0].ExpirationHeight }, }, { - "valid payout that does not equal missed payout", + "siacoin inputs (100 SC) do not equal outputs (101.04 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].HostOutput.Value = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "incorrect payout tax", + "siacoin inputs (100 SC) do not equal outputs (101 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(1)) @@ -1213,49 +1238,49 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "missed host value exceeding valid host value", + "file contract 0 has missed host value (2 SC) exceeding valid host value (1 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].MissedHostValue = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "total collateral exceeding valid host value", + "file contract 0 has total collateral (2 SC) exceeding valid host value (1 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].TotalCollateral = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "spends siacoin output not in accumulator", + fmt.Sprintf("siacoin input 0 spends output (%v) not present in the accumulator", sces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] - txn.SiacoinInputs[0].Parent.ID[0] ^= 255 + txn.SiacoinInputs[0].Parent.StateElement.LeafIndex ^= 1 }, }, { - "spends siafund output not in accumulator", + fmt.Sprintf("siafund input 0 spends output (%v) not present in the accumulator", sfes[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] - txn.SiafundInputs[0].Parent.ID[0] ^= 255 + txn.SiafundInputs[0].Parent.StateElement.LeafIndex ^= 1 }, }, { - "superfluous siacoin spend policy preimage(s)", + "siacoin input 0 failed to satisfy spend policy: superfluous preimage(s)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs[0].SatisfiedPolicy.Preimages = [][32]byte{{1}} }, }, { - "superfluous siafund spend policy preimage(s)", + "siafund input 0 failed to satisfy spend policy: superfluous preimage(s)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs[0].SatisfiedPolicy.Preimages = [][32]byte{{1}} }, }, { - "transaction both resolves a file contract and creates new outputs", + fmt.Sprintf("file contract renewal 0 parent (%v) has already been revised by contract revision", fces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractResolutions = append(txn.FileContractResolutions, types.V2FileContractResolution{ @@ -1265,14 +1290,14 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "attestation with an empty key", + "attestation 0 has empty key", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.Attestations = append(txn.Attestations, types.Attestation{}) }, }, { - "attestation with invalid signature", + "attestation 0 has invalid signature", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.Attestations = append(txn.Attestations, types.Attestation{ @@ -1291,8 +1316,8 @@ func TestValidateV2Block(t *testing.T) { } findBlockNonce(cs, &corruptBlock) - if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil { - t.Fatalf("accepted block with %v", test.desc) + if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("expected error containing %q, got %v", test.errString, err) } } } @@ -1962,7 +1987,7 @@ func TestV2RenewalResolution(t *testing.T) { renewal.HostRollover = types.ZeroCurrency txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.FinalRenterOutput.Value).Sub(renewal.FinalHostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) }, - errString: "siacoin inputs (1000000000000000000000000000 H) do not equal outputs (1001000000000000000000000000 H)", // this is an inputs != outputs error because the renewal is validated there first + errString: "siacoin inputs (1 KS) do not equal outputs (1.001 KS)", // this is an inputs != outputs error because the renewal is validated there first }, { desc: "invalid renewal - bad new contract renter signature", From b2c0b10be836dfd6e418e782a2dc2237ab161eff Mon Sep 17 00:00:00 2001 From: lukechampine Date: Mon, 16 Dec 2024 15:52:47 -0500 Subject: [PATCH 40/45] consensus: Expand ApplyUpdate/RevertUpdate docstrings --- consensus/application.go | 66 ++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/consensus/application.go b/consensus/application.go index 037845be..b10bf2f7 100644 --- a/consensus/application.go +++ b/consensus/application.go @@ -668,14 +668,20 @@ func (au ApplyUpdate) UpdateElementProof(e *types.StateElement) { au.eau.updateElementProof(e) } -// ForEachSiacoinElement calls fn on each siacoin element related to au. +// ForEachSiacoinElement calls fn on each siacoin element related to the applied +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (au ApplyUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, created, spent bool)) { for _, sce := range au.ms.sces { fn(sce, au.ms.isCreated(sce.ID), au.ms.isSpent(sce.ID)) } } -// ForEachSiafundElement calls fn on each siafund element related to au. +// ForEachSiafundElement calls fn on each siafund element related to the applied +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (au ApplyUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, created, spent bool)) { for _, sfe := range au.ms.sfes { fn(sfe, au.ms.isCreated(sfe.ID), au.ms.isSpent(sfe.ID)) @@ -683,16 +689,28 @@ func (au ApplyUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, cr } // ForEachFileContractElement calls fn on each file contract element related to -// au. If the contract was revised, rev is non-nil. +// the applied block. The created flag indicates whether the contract was newly +// created. If the contract was revised, rev is non-nil and represents the state +// of the element post-application. If the block revised the contract multiple +// times, rev is the revision with the highest revision number. The resolved and +// valid flags indicate whether the contract was resolved, and if so, whether it +// was resolved via storage proof. Note that a contract may be created, revised, +// and resolved all within the same block. func (au ApplyUpdate) ForEachFileContractElement(fn func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool)) { for _, fce := range au.ms.fces { fn(fce, au.ms.isCreated(fce.ID), au.ms.revs[fce.ID], au.ms.isSpent(fce.ID), au.ms.res[fce.ID]) } } -// ForEachV2FileContractElement calls fn on each V2 file contract element -// related to au. If the contract was revised, rev is non-nil. If the contract -// was resolved, res is non-nil. +// ForEachV2FileContractElement calls fn on each v2 file contract element +// related to the applied block. The created flag indicates whether the contract +// was newly created. If the contract was revised, rev is non-nil and represents +// the state of the element post-application. If the block revised the contract +// multiple times, rev is the revision with the highest revision number. The +// resolved and valid flags indicate whether the contract was resolved, and if +// so, whether it was resolved via storage proof. Note that, within a block, a +// contract may be created and revised, or revised and resolved, but not created +// and resolved. func (au ApplyUpdate) ForEachV2FileContractElement(fn func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType)) { for _, fce := range au.ms.v2fces { fn(fce, au.ms.isCreated(fce.ID), au.ms.v2revs[fce.ID], au.ms.v2res[fce.ID]) @@ -758,7 +776,11 @@ func ApplyBlock(s State, b types.Block, bs V1BlockSupplement, targetTimestamp ti return s, ApplyUpdate{ms, eau} } -// A RevertUpdate represents the effects of reverting to a prior state. +// A RevertUpdate represents the effects of reverting to a prior state. These +// are the same effects seen as when applying the block, but should be processed +// inversely. For example, if ForEachSiacoinElement reports an element with the +// created flag set, it means the block created that element when it was +// applied; thus, when the block is reverted, the element no longer exists. type RevertUpdate struct { ms *MidState eru elementRevertUpdate @@ -771,7 +793,10 @@ func (ru RevertUpdate) UpdateElementProof(e *types.StateElement) { ru.eru.updateElementProof(e) } -// ForEachSiacoinElement calls fn on each siacoin element related to ru. +// ForEachSiacoinElement calls fn on each siacoin element related to the reverted +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (ru RevertUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, created, spent bool)) { for i := range ru.ms.sces { sce := ru.ms.sces[len(ru.ms.sces)-i-1] @@ -779,7 +804,10 @@ func (ru RevertUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, c } } -// ForEachSiafundElement calls fn on each siafund element related to ru. +// ForEachSiafundElement calls fn on each siafund element related to the +// reverted block. The created and spent flags indicate whether the element was +// newly created in the block and/or spent in the block. Note that an element +// may be both created and spent in the the same block. func (ru RevertUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, created, spent bool)) { for i := range ru.ms.sfes { sfe := ru.ms.sfes[len(ru.ms.sfes)-i-1] @@ -788,7 +816,13 @@ func (ru RevertUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, c } // ForEachFileContractElement calls fn on each file contract element related to -// ru. If the contract was revised, rev is non-nil. +// the reverted block. The created flag indicates whether the contract was newly +// created. If the contract was revised, rev is non-nil and represents the state +// of the element post-application. If the block revised the contract multiple +// times, rev is the revision with the highest revision number. The resolved and +// valid flags indicate whether the contract was resolved, and if so, whether it +// was resolved via storage proof. Note that a contract may be created, revised, +// and resolved all within the same block. func (ru RevertUpdate) ForEachFileContractElement(fn func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool)) { for i := range ru.ms.fces { fce := ru.ms.fces[len(ru.ms.fces)-i-1] @@ -796,9 +830,15 @@ func (ru RevertUpdate) ForEachFileContractElement(fn func(fce types.FileContract } } -// ForEachV2FileContractElement calls fn on each V2 file contract element -// related to au. If the contract was revised, rev is non-nil. If the contract -// was resolved, res is non-nil. +// ForEachV2FileContractElement calls fn on each v2 file contract element +// related to the reverted block. The created flag indicates whether the +// contract was newly created. If the contract was revised, rev is non-nil and +// represents the state of the element post-application. If the block revised +// the contract multiple times, rev is the revision with the highest revision +// number. The resolved and valid flags indicate whether the contract was +// resolved, and if so, whether it was resolved via storage proof. Note that, +// within a block, a contract may be created and revised, or revised and +// resolved, but not created and resolved. func (ru RevertUpdate) ForEachV2FileContractElement(fn func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType)) { for i := range ru.ms.v2fces { fce := ru.ms.v2fces[len(ru.ms.v2fces)-i-1] From a3f2c653b8677a285cb0af53fbe80d4588a62bd5 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Tue, 17 Dec 2024 20:23:59 -0500 Subject: [PATCH 41/45] consensus: Add early storage proof test case --- consensus/state.go | 2 +- consensus/validation.go | 2 +- consensus/validation_test.go | 30 ++++++++++++++++++++++++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index f5c5c370..41835adf 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -631,7 +631,7 @@ func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.Fil } func (ms *MidState) storageProof(ts V1TransactionSupplement, id types.FileContractID) (V1StorageProofSupplement, bool) { - if i, ok := ms.created[id]; ok && ms.fces[i].FileContract.WindowStart == ms.base.childHeight() { + if i, ok := ms.created[id]; ok { return V1StorageProofSupplement{FileContract: ms.fces[i], WindowID: ms.base.Index.ID}, true } return ts.storageProof(id) diff --git a/consensus/validation.go b/consensus/validation.go index 202ea1bc..307e1d13 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -90,7 +90,7 @@ func ValidateOrphan(s State, b types.Block) error { } else if err := validateMinerPayouts(s, b); err != nil { return err } else if err := validateHeader(s, b.ParentID, b.Timestamp, b.Nonce, b.ID()); err != nil { - return fmt.Errorf("block has %v", err) + return fmt.Errorf("block has %w", err) } if b.V2 != nil { if b.V2.Height != s.Index.Height+1 { diff --git a/consensus/validation_test.go b/consensus/validation_test.go index 03c36cf4..057ce8e2 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -594,20 +594,38 @@ func TestValidateBlock(t *testing.T) { }, { "storage proof 0 references nonexistent file contract", + func(b *types.Block) { + txn := &b.Transactions[1] + txn.StorageProofs[0].ParentID = types.FileContractID{} + }, + }, + { + "storage proof 0 cannot be submitted until after window start (100)", + func(b *types.Block) { + b.Transactions[0].FileContracts[0].WindowStart = 100 + b.Transactions[1].StorageProofs[0].ParentID = b.Transactions[0].FileContractID(0) + }, + }, + { + fmt.Sprintf("storage proof 1 resolves contract (%v) already resolved by storage proof 0", b.Transactions[0].FileContractID(0)), + func(b *types.Block) { + txn := &b.Transactions[1] + txn.StorageProofs = append(txn.StorageProofs, txn.StorageProofs[0]) + }, + }, + { + fmt.Sprintf("storage proof 0 conflicts with previous proof (in %v)", b.Transactions[1].ID()), func(b *types.Block) { b.Transactions = append(b.Transactions, types.Transaction{ - StorageProofs: []types.StorageProof{{}}, + StorageProofs: b.Transactions[1].StorageProofs, }) }, }, { - "storage proof 0 conflicts with previous proof", + fmt.Sprintf("storage proof 0 conflicts with previous proof (in %v)", b.Transactions[1].ID()), func(b *types.Block) { - txn := &b.Transactions[0] b.Transactions = append(b.Transactions, types.Transaction{ - StorageProofs: []types.StorageProof{{ - ParentID: txn.FileContractID(0), - }}, + StorageProofs: b.Transactions[1].StorageProofs, }) }, }, From caa8ba650903f9c8728ab488f177cf660e237b03 Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 18 Dec 2024 10:02:33 -0500 Subject: [PATCH 42/45] consensus: Split (MidState).storageProof into two methods --- consensus/application.go | 6 +++--- consensus/state.go | 17 ++++++++++------- consensus/validation.go | 9 +++++---- consensus/validation_test.go | 13 +++++++++++++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/consensus/application.go b/consensus/application.go index b10bf2f7..fe273a39 100644 --- a/consensus/application.go +++ b/consensus/application.go @@ -495,12 +495,12 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp ms.reviseFileContractElement(fce, fcr.FileContract) } for _, sp := range txn.StorageProofs { - sps, ok := ms.storageProof(ts, sp.ParentID) + fce, ok := ms.fileContractElement(ts, sp.ParentID) if !ok { panic("missing V1StorageProofSupplement") } - ms.resolveFileContractElement(sps.FileContract, true, txid) - for i, sco := range sps.FileContract.FileContract.ValidProofOutputs { + ms.resolveFileContractElement(fce, true, txid) + for i, sco := range fce.FileContract.ValidProofOutputs { ms.addImmatureSiacoinElement(sp.ParentID.ValidOutputID(i), sco) } } diff --git a/consensus/state.go b/consensus/state.go index 41835adf..7c984570 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -623,18 +623,21 @@ func (ms *MidState) siafundElement(ts V1TransactionSupplement, id types.SiafundO func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.FileContractID) (types.FileContractElement, bool) { if rev, ok := ms.revs[id]; ok { return *rev, true - } - if i, ok := ms.created[id]; ok { + } else if i, ok := ms.created[id]; ok { return ms.fces[i], true + } else if rev, ok := ts.revision(id); ok { + return rev, true } - return ts.revision(id) + sps, ok := ts.storageProof(id) + return sps.FileContract, ok } -func (ms *MidState) storageProof(ts V1TransactionSupplement, id types.FileContractID) (V1StorageProofSupplement, bool) { - if i, ok := ms.created[id]; ok { - return V1StorageProofSupplement{FileContract: ms.fces[i], WindowID: ms.base.Index.ID}, true +func (ms *MidState) storageProofWindowID(ts V1TransactionSupplement, id types.FileContractID) (types.BlockID, bool) { + if i, ok := ms.created[id]; ok && ms.fces[i].FileContract.WindowStart == ms.base.childHeight() { + return ms.base.Index.ID, true } - return ts.storageProof(id) + sps, ok := ts.storageProof(id) + return sps.WindowID, ok } func (ms *MidState) spent(id types.ElementID) (types.TransactionID, bool) { diff --git a/consensus/validation.go b/consensus/validation.go index 307e1d13..cb200ea9 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -354,15 +354,16 @@ func validateFileContracts(ms *MidState, txn types.Transaction, ts V1Transaction if txid, ok := ms.spent(sp.ParentID); ok { return fmt.Errorf("storage proof %v conflicts with previous proof (in %v)", i, txid) } - sps, ok := ms.storageProof(ts, sp.ParentID) + fce, ok := ms.fileContractElement(ts, sp.ParentID) if !ok { return fmt.Errorf("storage proof %v references nonexistent file contract", i) } - fc := sps.FileContract.FileContract - if ms.base.childHeight() < fc.WindowStart { + fc := fce.FileContract + windowID, ok := ms.storageProofWindowID(ts, sp.ParentID) + if !ok { return fmt.Errorf("storage proof %v cannot be submitted until after window start (%v)", i, fc.WindowStart) } - leafIndex := ms.base.StorageProofLeafIndex(fc.Filesize, sps.WindowID, sp.ParentID) + leafIndex := ms.base.StorageProofLeafIndex(fc.Filesize, windowID, sp.ParentID) leaf := storageProofLeaf(leafIndex, fc.Filesize, sp.Leaf) if leaf == nil { continue diff --git a/consensus/validation_test.go b/consensus/validation_test.go index 057ce8e2..8db0a60e 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -606,6 +606,19 @@ func TestValidateBlock(t *testing.T) { b.Transactions[1].StorageProofs[0].ParentID = b.Transactions[0].FileContractID(0) }, }, + { + "file contract revision 0 conflicts with previous proof or revision", + func(b *types.Block) { + rev := revision + rev.RevisionNumber++ + b.Transactions = append(b.Transactions, types.Transaction{ + FileContractRevisions: []types.FileContractRevision{{ + ParentID: b.Transactions[1].StorageProofs[0].ParentID, + FileContract: rev, + }}, + }) + }, + }, { fmt.Sprintf("storage proof 1 resolves contract (%v) already resolved by storage proof 0", b.Transactions[0].FileContractID(0)), func(b *types.Block) { From b94782a2c4439fb710068cbfaba0f42a4f7587bc Mon Sep 17 00:00:00 2001 From: lukechampine Date: Wed, 18 Dec 2024 10:07:53 -0500 Subject: [PATCH 43/45] consensus: Inline V1TransactionSupplement methods --- consensus/state.go | 73 ++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index 7c984570..6cebe88f 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -610,14 +610,24 @@ func (ms *MidState) siacoinElement(ts V1TransactionSupplement, id types.SiacoinO if i, ok := ms.created[id]; ok { return ms.sces[i], true } - return ts.siacoinElement(id) + for _, sce := range ts.SiacoinInputs { + if sce.ID == id { + return sce, true + } + } + return types.SiacoinElement{}, false } func (ms *MidState) siafundElement(ts V1TransactionSupplement, id types.SiafundOutputID) (types.SiafundElement, bool) { if i, ok := ms.created[id]; ok { return ms.sfes[i], true } - return ts.siafundElement(id) + for _, sfe := range ts.SiafundInputs { + if sfe.ID == id { + return sfe, true + } + } + return types.SiafundElement{}, false } func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.FileContractID) (types.FileContractElement, bool) { @@ -625,19 +635,30 @@ func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.Fil return *rev, true } else if i, ok := ms.created[id]; ok { return ms.fces[i], true - } else if rev, ok := ts.revision(id); ok { - return rev, true } - sps, ok := ts.storageProof(id) - return sps.FileContract, ok + for _, fce := range ts.RevisedFileContracts { + if fce.ID == id { + return fce, true + } + } + for _, sps := range ts.StorageProofs { + if sps.FileContract.ID == id { + return sps.FileContract, true + } + } + return types.FileContractElement{}, false } func (ms *MidState) storageProofWindowID(ts V1TransactionSupplement, id types.FileContractID) (types.BlockID, bool) { if i, ok := ms.created[id]; ok && ms.fces[i].FileContract.WindowStart == ms.base.childHeight() { return ms.base.Index.ID, true } - sps, ok := ts.storageProof(id) - return sps.WindowID, ok + for _, sps := range ts.StorageProofs { + if sps.FileContract.ID == id { + return sps.WindowID, true + } + } + return types.BlockID{}, false } func (ms *MidState) spent(id types.ElementID) (types.TransactionID, bool) { @@ -720,42 +741,6 @@ func (ts *V1TransactionSupplement) DecodeFrom(d *types.Decoder) { types.DecodeSlice(d, &ts.StorageProofs) } -func (ts V1TransactionSupplement) siacoinElement(id types.SiacoinOutputID) (sce types.SiacoinElement, ok bool) { - for _, sce := range ts.SiacoinInputs { - if sce.ID == id { - return sce, true - } - } - return -} - -func (ts V1TransactionSupplement) siafundElement(id types.SiafundOutputID) (sfe types.SiafundElement, ok bool) { - for _, sfe := range ts.SiafundInputs { - if sfe.ID == id { - return sfe, true - } - } - return -} - -func (ts V1TransactionSupplement) revision(id types.FileContractID) (fce types.FileContractElement, ok bool) { - for _, fce := range ts.RevisedFileContracts { - if fce.ID == id { - return fce, true - } - } - return -} - -func (ts V1TransactionSupplement) storageProof(id types.FileContractID) (sps V1StorageProofSupplement, ok bool) { - for _, sps := range ts.StorageProofs { - if sps.FileContract.ID == id { - return sps, true - } - } - return -} - // A V1BlockSupplement contains elements that are associated with a v1 block, // but not included in the block. This includes supplements for each v1 // transaction, as well as any file contracts that expired at the block's From 0499472331e9bc8ee5201c84402ba79fd793dfba Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 18 Dec 2024 08:43:33 -0800 Subject: [PATCH 44/45] add changeset for #258 --- .changeset/allow_v1_contracts_to_be_resolved_immediately.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/allow_v1_contracts_to_be_resolved_immediately.md diff --git a/.changeset/allow_v1_contracts_to_be_resolved_immediately.md b/.changeset/allow_v1_contracts_to_be_resolved_immediately.md new file mode 100644 index 00000000..b04a5fa0 --- /dev/null +++ b/.changeset/allow_v1_contracts_to_be_resolved_immediately.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +# Allow v1 contracts to be resolved immediately From e56ae373f5b7fe0fdab33fe8f912a0c2560c92ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:51:04 +0000 Subject: [PATCH 45/45] chore: prepare release 0.9.0 --- .../add_helper_for_generating_account_tokens.md | 5 ----- .changeset/add_host_public_key_to_accounttoken.md | 5 ----- ...llow_v1_contracts_to_be_resolved_immediately.md | 5 ----- CHANGELOG.md | 14 ++++++++++++++ go.mod | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) delete mode 100644 .changeset/add_helper_for_generating_account_tokens.md delete mode 100644 .changeset/add_host_public_key_to_accounttoken.md delete mode 100644 .changeset/allow_v1_contracts_to_be_resolved_immediately.md diff --git a/.changeset/add_helper_for_generating_account_tokens.md b/.changeset/add_helper_for_generating_account_tokens.md deleted file mode 100644 index 404b749f..00000000 --- a/.changeset/add_helper_for_generating_account_tokens.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -default: minor ---- - -# Add helper for generating account tokens diff --git a/.changeset/add_host_public_key_to_accounttoken.md b/.changeset/add_host_public_key_to_accounttoken.md deleted file mode 100644 index ad58d59f..00000000 --- a/.changeset/add_host_public_key_to_accounttoken.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -default: major ---- - -# Add host public key to AccountToken \ No newline at end of file diff --git a/.changeset/allow_v1_contracts_to_be_resolved_immediately.md b/.changeset/allow_v1_contracts_to_be_resolved_immediately.md deleted file mode 100644 index b04a5fa0..00000000 --- a/.changeset/allow_v1_contracts_to_be_resolved_immediately.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -default: patch ---- - -# Allow v1 contracts to be resolved immediately diff --git a/CHANGELOG.md b/CHANGELOG.md index 6390a2e0..f8a33b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.9.0 (2024-12-18) + +### Breaking Changes + +- Add host public key to AccountToken + +### Features + +- Add helper for generating account tokens + +### Fixes + +- Allow v1 contracts to be resolved immediately + ## 0.8.0 (2024-12-13) ### Breaking Changes diff --git a/go.mod b/go.mod index 1e3de03b..e0b0b2b0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core // v0.8.0 +module go.sia.tech/core // v0.9.0 go 1.23.1