diff --git a/cmd/rivined/daemon.go b/cmd/rivined/daemon.go index b35aace20..986b09d7d 100644 --- a/cmd/rivined/daemon.go +++ b/cmd/rivined/daemon.go @@ -18,6 +18,7 @@ import ( "github.com/threefoldtech/rivine/modules/wallet" "github.com/threefoldtech/rivine/pkg/api" "github.com/threefoldtech/rivine/pkg/daemon" + "github.com/threefoldtech/rivine/types" ) func runDaemon(cfg daemon.Config, networkCfg daemon.NetworkConfig, moduleIdentifiers daemon.ModuleIdentifierSet) error { @@ -85,6 +86,7 @@ func runDaemon(cfg daemon.Config, networkCfg daemon.NetworkConfig, moduleIdentif fmt.Println("Error during consensus set shutdown:", err) } }() + types.RegisterTransactionVersion(types.TransactionVersionBlockCreation, types.NewBlockCreationTransactionController(cs)) } var tpool modules.TransactionPool diff --git a/modules/blockcreator/proofofblockstake.go b/modules/blockcreator/proofofblockstake.go index 998e501dd..e86e53038 100644 --- a/modules/blockcreator/proofofblockstake.go +++ b/modules/blockcreator/proofofblockstake.go @@ -2,6 +2,7 @@ package blockcreator import ( "encoding/json" + "errors" "math/big" "time" @@ -157,24 +158,43 @@ func (bc *BlockCreator) RespentBlockStake(ubso types.UnspentBlockStakeOutput) er } } - //otherwise the blockstake is not yet spent in this block, spent it now - t := bc.wallet.StartTransaction() - err := t.SpendBlockStake(ubso.BlockStakeOutputID) // link the input of this transaction - // to the used BlockStake output + // No valid blockstake have been spend yet in the block to be created + + // get current height so we can store the block height of the block the tx is + // supposed to make in the block (currentHeight + 1) + currentHeight := bc.cs.Height() + + bso, err := bc.cs.GetBlockStakeOutput(ubso.BlockStakeOutputID) if err != nil { return err } - bso := types.BlockStakeOutput{ - Value: ubso.Value, //use the same amount of BlockStake - Condition: ubso.Condition, //use the same condition. + if bso.Condition.ConditionType() != types.ConditionTypeUnlockHash { + return errors.New("unsupported output condition for pobs") } - t.AddBlockStakeOutput(bso) - txnSet, err := t.Sign() + + pk, _, err := bc.wallet.GetKey(bso.Condition.UnlockHash()) if err != nil { + return errors.New("Failed to retrieve public key for blockstake output") + } + + // create the input for the used output + input := types.BlockStakeInput{ParentID: ubso.BlockStakeOutputID, Fulfillment: types.NewFulfillment(types.NewSingleSignatureFulfillment(pk))} + + // create the extension for the block creation tx based on the used input + txExt := &types.BlockCreationTransactionExtension{Reference: input, Height: types.BlockHeight(currentHeight + 1)} + + // Create transactionbuilder for the new transaction and add the extension + txb := bc.wallet.StartTransactionWithVersion(types.TransactionVersionBlockCreation) + txb.SetExtension(txExt) + if err = txb.SignAllPossible(); err != nil { return err } - //add this transaction in front of the list of unsolved block transactions - bc.unsolvedBlock.Transactions = append(txnSet, bc.unsolvedBlock.Transactions...) + + tx, _ := txb.View() + + // add this transaction in front of the list of unsolved block transactions + bc.unsolvedBlock.Transactions = append([]types.Transaction{tx}, bc.unsolvedBlock.Transactions...) + return nil } diff --git a/modules/consensus/block_validation.go b/modules/consensus/block_validation.go index ea06ac0ed..23e69db11 100644 --- a/modules/consensus/block_validation.go +++ b/modules/consensus/block_validation.go @@ -107,6 +107,17 @@ func (bv stdBlockValidator) ValidateBlock(b types.Block, minTimestamp types.Time spent = true } } + // check block creation txes, as they don't technically respend + if tr.Version == types.TransactionVersionBlockCreation { + ext, ok := tr.Extension.(*types.BlockCreationTransactionExtension) + if !ok { + // should never happen + continue + } + if ext.Reference.ParentID == bsoid { + spent = true + } + } } } } @@ -123,6 +134,18 @@ func (bv stdBlockValidator) ValidateBlock(b types.Block, minTimestamp types.Time spent = true } } + // check block creation txes, as they don't technically respend + if tr.Version == types.TransactionVersionBlockCreation { + ext, ok := tr.Extension.(*types.BlockCreationTransactionExtension) + if !ok { + // should never happen + continue + } + if ext.Reference.ParentID == blockatheight.Transactions[ubsu.TransactionIndex].BlockStakeOutputID(ubsu.OutputIndex) { + bv.cs.log.Debugf("[SBV] Confirmed blockstake respend from an inactive fork, ubsu in block %d, new block at height %d\n", ubsu.BlockHeight, height) + spent = true + } + } } } } diff --git a/modules/wallet.go b/modules/wallet.go index f120a4a8e..047cc4e8c 100644 --- a/modules/wallet.go +++ b/modules/wallet.go @@ -192,6 +192,9 @@ type ( // AddArbitraryData sets the arbitrary data of the transaction. SetArbitraryData(arb []byte) + // SetExtension sets the extension of the transaction + SetExtension(interface{}) + // Sign will sign any inputs added by 'FundCoins' or 'FundBlockStakes' // and return a transaction set that contains all parents prepended to // the transaction. If more fields need to be added, a new transaction @@ -389,6 +392,10 @@ type ( // RegisterTransaction(types.Transaction{}, nil) StartTransaction() TransactionBuilder + // StartTransactionWithVersion is a convenience function that calls + // RegisterTransaction(types.Transaction{Version: version}, nil). + StartTransactionWithVersion(types.TransactionVersion) TransactionBuilder + // SendCoins is a tool for sending coins from the wallet to anyone who can fulfill the // given condition (can be nil). The transaction is automatically given to the transaction pool, and // are also returned to the caller. diff --git a/modules/wallet/transactionbuilder.go b/modules/wallet/transactionbuilder.go index 511ceff92..1e8ef3b53 100644 --- a/modules/wallet/transactionbuilder.go +++ b/modules/wallet/transactionbuilder.go @@ -354,6 +354,11 @@ func (tb *transactionBuilder) SetArbitraryData(arb []byte) { tb.transaction.ArbitraryData = arb } +// SetExtension sets the extension of the transaction +func (tb *transactionBuilder) SetExtension(extension interface{}) { + tb.transaction.Extension = extension +} + // Drop discards all of the outputs in a transaction, returning them to the // pool so that other transactions may use them. 'Drop' should only be called // if a transaction is both unsigned and will not be used any further. diff --git a/types/transactions_blockcreation.go b/types/transactions_blockcreation.go new file mode 100644 index 000000000..86b811cb6 --- /dev/null +++ b/types/transactions_blockcreation.go @@ -0,0 +1,314 @@ +package types + +import ( + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/threefoldtech/rivine/crypto" + "github.com/threefoldtech/rivine/pkg/encoding/rivbin" +) + +// These Specifiers are used internally when calculating a type's ID. See +// Specifier for more details. +var ( + SpecifierBlockCreationTransaction = Specifier{'b', 'l', 'o', 'c', 'k', ' ', 'c', 'r', 'e', 'a', 't', 'i', 'o', 'n'} +) + +const ( + // TransactionVersionBlockCreation defines the special transaction used for + // creating blocks by referencing an owned blockstake output + TransactionVersionBlockCreation TransactionVersion = 2 +) + +type ( + // BlockCreationTransaction defines the transaction (with version 0x02) + // used to create a new block by proving ownership of a referenced + // blockstake output + BlockCreationTransaction struct { + // Reference unlocks a blockstake output to prove ownership, but does not consume it + Reference BlockStakeInput + // Height for the block this tx is going to create + Height BlockHeight + } + + // BlockCreationTransactionExtension defines the BlockCreationTransaction Extension Data + BlockCreationTransactionExtension struct { + // Reference unlocks a blockstake output to prove ownership, but does not consume it + Reference BlockStakeInput + // Height for the block this tx is going to create + Height BlockHeight + } +) + +// BlockCreationTransactionFromTransaction creates a BlockCreationTransaction, using a regular in-memory rivine transaction +func BlockCreationTransactionFromTransaction(tx Transaction) (BlockCreationTransaction, error) { + if tx.Version != TransactionVersionBlockCreation { + return BlockCreationTransaction{}, fmt.Errorf( + "block creation transaction requires tx version %d", TransactionVersionBlockCreation, + ) + } + return BlockCreationTransactionFromTransactionData(TransactionData{ + CoinInputs: tx.CoinInputs, + CoinOutputs: tx.CoinOutputs, + BlockStakeInputs: tx.BlockStakeInputs, + BlockStakeOutputs: tx.BlockStakeOutputs, + MinerFees: tx.MinerFees, + ArbitraryData: tx.ArbitraryData, + Extension: tx.Extension, + }) +} + +// BlockCreationTransactionFromTransactionData creates an BlockCreationTransaction, +// using the TransactionData from a regular in-memory rivine transaction. +func BlockCreationTransactionFromTransactionData(txData TransactionData) (BlockCreationTransaction, error) { + // validate transaction data + + // no coin inputs or outputs allowed + if len(txData.CoinInputs) != 0 || len(txData.CoinOutputs) != 0 { + return BlockCreationTransaction{}, errors.New("no coin input or outputs allowed for a block creation transaction") + } + + // no miner fee allowed + if len(txData.MinerFees) != 0 { + return BlockCreationTransaction{}, errors.New("no transaction fees allowed for a block creation transaction") + } + + // no arb data allowed + if len(txData.ArbitraryData) != 0 { + return BlockCreationTransaction{}, errors.New("no arbitrary data allowed for a block creation transaction") + } + + // no blockstake input and outputs allowed, can't move block stakes + if len(txData.BlockStakeOutputs) != 0 || len(txData.BlockStakeInputs) != 0 { + return BlockCreationTransaction{}, errors.New("no blockstake outputs allowed for a block creation transaction") + } + + tx := BlockCreationTransaction{} + + if txData.Extension != nil { + extensionData, ok := txData.Extension.(*BlockCreationTransactionExtension) + if !ok { + return tx, errors.New("invalid extension data for a block creation transaction") + } + + tx.Reference = extensionData.Reference + tx.Height = extensionData.Height + } + + return tx, nil +} + +// TransactionData returns this BlockCreationTransaction +// as regular rivine transaction data. +func (bctx *BlockCreationTransaction) TransactionData() TransactionData { + txData := TransactionData{ + Extension: &BlockCreationTransactionExtension{ + Reference: bctx.Reference, + Height: bctx.Height, + }, + } + return txData +} + +// Transaction returns this BlockCreationTransaction +// as regular rivine transaction, using TransactionVersionBlockCreation as the type. +func (bctx *BlockCreationTransaction) Transaction() Transaction { + tx := Transaction{ + Version: TransactionVersionBlockCreation, + Extension: &BlockCreationTransactionExtension{ + Reference: bctx.Reference, + Height: bctx.Height, + }, + } + return tx +} + +// MarshalSia implements SiaMarshaler.MarshalSia, +// alias of MarshalRivine for backwards-compatibility reasons. +func (bctx BlockCreationTransaction) MarshalSia(w io.Writer) error { + return bctx.MarshalRivine(w) +} + +// UnmarshalSia implements SiaUnmarshaler.UnmarshalSia, +// alias of UnmarshalRivine for backwards-compatibility reasons. +func (bctx *BlockCreationTransaction) UnmarshalSia(r io.Reader) error { + return bctx.UnmarshalRivine(r) +} + +// MarshalRivine implements RivineMarshaler.MarshalRivine +func (bctx BlockCreationTransaction) MarshalRivine(w io.Writer) error { + return rivbin.NewEncoder(w).EncodeAll( + bctx.Reference, + ) +} + +// UnmarshalRivine implements RivineUnmarshaler.UnmarshalRivine +func (bctx *BlockCreationTransaction) UnmarshalRivine(r io.Reader) error { + return rivbin.NewDecoder(r).DecodeAll( + &bctx.Reference, + ) +} + +type ( + // BlockCreationTransactionController defines a transaction controller for a a transaction type + // reserved at type 0x02. It allows creation of blocks without blockstake respend + BlockCreationTransactionController struct { + bsog BlockStakeOutputGetter + } + + //BlockStakeOutputGetter allows the retrieval of a blockstake output based on its ID + BlockStakeOutputGetter interface { + GetBlockStakeOutput(BlockStakeOutputID) (BlockStakeOutput, error) + } +) + +var ( + // ensure at compile time that BlockCreationTransactionController + // implements the desired interfaces + _ TransactionController = BlockCreationTransactionController{} + _ TransactionValidator = BlockCreationTransactionController{} + _ TransactionSignatureHasher = BlockCreationTransactionController{} + _ TransactionIDEncoder = BlockCreationTransactionController{} + _ TransactionExtensionSigner = BlockCreationTransactionController{} +) + +// NewBlockCreationTransactionController creates a new block creation transaction controller +func NewBlockCreationTransactionController(bsog BlockStakeOutputGetter) BlockCreationTransactionController { + return BlockCreationTransactionController{ + bsog: bsog, + } +} + +// EncodeTransactionData implements TransactionController.EncodeTransactionData +func (bctc BlockCreationTransactionController) EncodeTransactionData(w io.Writer, txData TransactionData) error { + bctx, err := BlockCreationTransactionFromTransactionData(txData) + if err != nil { + return fmt.Errorf("failed to convert txData to a BlockCreationTx: %v", err) + } + return rivbin.NewEncoder(w).Encode(bctx) +} + +// DecodeTransactionData implements TransactionController.DecodeTransactionData +func (bctc BlockCreationTransactionController) DecodeTransactionData(r io.Reader) (TransactionData, error) { + var bctx BlockCreationTransaction + err := rivbin.NewDecoder(r).Decode(&bctx) + if err != nil { + return TransactionData{}, fmt.Errorf( + "failed to binary-decode tx as a BlockCreationTx: %v", err) + } + // return block creation tx as regular rivine tx data + return bctx.TransactionData(), nil +} + +// JSONEncodeTransactionData implements TransactionController.JSONEncodeTransactionData +func (bctc BlockCreationTransactionController) JSONEncodeTransactionData(txData TransactionData) ([]byte, error) { + bctx, err := BlockCreationTransactionFromTransactionData(txData) + if err != nil { + return nil, fmt.Errorf("failed to convert txData to a BlockCreationTx: %v", err) + } + return json.Marshal(bctx) +} + +// JSONDecodeTransactionData implements TransactionController.JSONDecodeTransactionData +func (bctc BlockCreationTransactionController) JSONDecodeTransactionData(data []byte) (TransactionData, error) { + var bctx BlockCreationTransaction + err := json.Unmarshal(data, &bctx) + if err != nil { + return TransactionData{}, fmt.Errorf( + "failed to json-decode tx as a BlockCreationTx:: %v", err) + } + // return block creation tx as regular rivine tx data + return bctx.TransactionData(), nil +} + +// ValidateTransaction implements TransactionValidator.ValidateTransaction +func (bctc BlockCreationTransactionController) ValidateTransaction(t Transaction, ctx ValidationContext, constants TransactionValidationConstants) error { + // check tx fits within a block + err := TransactionFitsInABlock(t, constants.BlockSizeLimit) + if err != nil { + return err + } + + // get BlockCreationTx + bctx, err := BlockCreationTransactionFromTransaction(t) + if err != nil { + return fmt.Errorf("failed to use tx as a BlockCreationTx: %v", err) + } + + // check if the reference is a standard fulfillment + if err = bctx.Reference.Fulfillment.IsStandardFulfillment(ctx); err != nil { + return err + } + + bso, err := bctc.bsog.GetBlockStakeOutput(bctx.Reference.ParentID) + if err != nil { + return fmt.Errorf("failed to get the referenced blockstake output condition condition: %v", err) + } + + if err = bso.Condition.Fulfill(bctx.Reference.Fulfillment, FulfillContext{BlockHeight: ctx.BlockHeight, BlockTime: ctx.BlockTime, Transaction: t, ExtraObjects: nil}); err != nil { + return err + } + + // check block height in tx + if bctx.Height != ctx.BlockHeight { + return fmt.Errorf("tx is supposed to create block %v, but is in block %v", bctx.Height, ctx.BlockHeight) + } + + // Tx is valid + return nil +} + +// SignatureHash implements TransactionSignatureHasher.SignatureHash +func (bctc BlockCreationTransactionController) SignatureHash(t Transaction, extraObjects ...interface{}) (crypto.Hash, error) { + bctx, err := BlockCreationTransactionFromTransaction(t) + if err != nil { + return crypto.Hash{}, fmt.Errorf("failed to use tx as a BlockCreationTx: %v", err) + } + + h := crypto.NewHash() + enc := rivbin.NewEncoder(h) + + enc.EncodeAll( + t.Version, + SpecifierBlockCreationTransaction, + bctx.Reference.ParentID, + bctx.Height, + ) + + if len(extraObjects) > 0 { + enc.EncodeAll(extraObjects...) + } + + var hash crypto.Hash + h.Sum(hash[:0]) + return hash, nil +} + +// EncodeTransactionIDInput implements TransactionIDEncoder.EncodeTransactionIDInput +func (bctc BlockCreationTransactionController) EncodeTransactionIDInput(w io.Writer, txData TransactionData) error { + bctx, err := BlockCreationTransactionFromTransactionData(txData) + if err != nil { + return fmt.Errorf("failed to convert txData to a BlockCreationTx: %v", err) + } + return rivbin.NewEncoder(w).EncodeAll(SpecifierBlockCreationTransaction, bctx) +} + +// SignExtension implements TransactionExtensionSigner.SignExtension +func (bctc BlockCreationTransactionController) SignExtension(extension interface{}, sign func(*UnlockFulfillmentProxy, UnlockConditionProxy, ...interface{}) error) (interface{}, error) { + bctxExtension, ok := extension.(*BlockCreationTransactionExtension) + if !ok { + return nil, errors.New("Invalid extension data for a block creation transaction") + } + + bso, err := bctc.bsog.GetBlockStakeOutput(bctxExtension.Reference.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get the referenced blockstake output condition condition: %v", err) + } + err = sign(&bctxExtension.Reference.Fulfillment, bso.Condition) + if err != nil { + return nil, fmt.Errorf("failed to sign block creation tx extension: %v", err) + } + return bctxExtension, nil +}