Skip to content
Merged

NFTs #41

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
4aedf12
Add new transactions for NFTs.
redpartyhat May 26, 2021
817e274
Begin adding connect / disconnect logic.
redpartyhat May 27, 2021
dab7303
Create global param NFT fee.
redpartyhat May 27, 2021
f5639af
Basic create NFT test.
redpartyhat May 27, 2021
2f13d1b
Clean up basic NFT test a bit.
redpartyhat May 27, 2021
ab54434
Add test to confirm that modifying an NFT is not allowed. Remove para…
redpartyhat May 28, 2021
021e692
Add more NFT tests.
redpartyhat May 28, 2021
b8958f2
Add db_utils for NFT entries.
redpartyhat May 29, 2021
df93913
Start implementing bids.
redpartyhat May 29, 2021
36f5aa3
Add DB / utxoView functions for NFT bids.
redpartyhat May 30, 2021
65de812
Add disconnect for NFT bids.
redpartyhat May 30, 2021
271c389
Add more test.
redpartyhat May 30, 2021
1f0be93
Add first NFT bid test cases. Add missing flush code for NFT bids.
redpartyhat May 30, 2021
57f2f3d
Check that bids are making it into the DB properly.
redpartyhat May 31, 2021
3b07c42
Fill in _connect for accepting NFT bids.
redpartyhat May 31, 2021
ccd181c
Fix failing test.
redpartyhat May 31, 2021
285cea6
Add connect / disconnect for updating NFTs.
redpartyhat May 31, 2021
a8cc00a
More tests and stuff.
redpartyhat Jun 1, 2021
997c55b
Add unlockable text & bidder inputs to AcceptNFTBidMetadata.
redpartyhat Jun 2, 2021
7ca6af1
Add logic for paying royalties.
redpartyhat Jun 4, 2021
61848db
Merge branch 'main' into rph/nifties
redpartyhat Jun 4, 2021
989cc46
Merge branch 'main' into rph/nifties
redpartyhat Jun 7, 2021
e558056
Add royalties test.
redpartyhat Jun 8, 2021
16ece18
Merge branch 'main' into rph/nifties
redpartyhat Jun 8, 2021
4060fe6
Add a test to make sure that no royalties are paid to coins that do n…
redpartyhat Jun 8, 2021
3dc80f3
Add tests for bidding on serial number zero.
redpartyhat Jun 8, 2021
e371d4b
Add an index to track bitclout balance per public key.
redpartyhat Jun 11, 2021
ca38737
Merge branch 'main' into rph/nifties
redpartyhat Jun 11, 2021
c9284bb
Fix merge conflict.
redpartyhat Jun 15, 2021
15bb74a
Add ability to set IsForSale=false when creating an NFT and add MinBi…
redpartyhat Jun 16, 2021
37fe667
Merge branch 'main' into rph/nifties
redpartyhat Jun 16, 2021
4acd881
Merge branch 'main' into rph/nifties
redpartyhat Jun 19, 2021
5956ce2
Fix nits.
redpartyhat Jun 24, 2021
f2e667a
Update index to include the bidderPKID in the key, not the value.
redpartyhat Jun 27, 2021
5d65f7a
Add error handling for GetBitCloutBalanceNanosForPublicKey().
redpartyhat Jun 27, 2021
6885366
Handle DbGetBlockRewardForPublicKeyBlockHash error.
redpartyhat Jun 27, 2021
fb0089f
Add more error case tests for NFTs.
redpartyhat Jun 27, 2021
5a5b8fb
Add simple test to make sure min bid amount behaves correctly.
redpartyhat Jun 27, 2021
2a8dfd5
Add test to make sure NFT bids are deleted after accepting a winning …
redpartyhat Jun 28, 2021
d692198
Add a test to make sure that different min bid amounts for different …
redpartyhat Jun 28, 2021
ddc683b
Make MaxCopiesPerNFT a global param.
redpartyhat Jun 28, 2021
b6925de
Fix coin royalty bug in creating NFT.
redpartyhat Jun 30, 2021
561f8a1
Apply suggestions from code review
redpartyhat Jun 30, 2021
13090e0
Fix failing tests.
redpartyhat Jul 1, 2021
ea5b3dc
Ln/nifties (#54)
lazynina Jul 6, 2021
e870341
don't allow nfts to be minted on vanilla reclouts
lazynina Jul 8, 2021
1e12f72
Add check to make sure bid entries are deleted after accepting a bid …
redpartyhat Jul 8, 2021
7ef6d9b
Add test to ensure a serial #0 bid can be submitted when no copies of…
redpartyhat Jul 8, 2021
1fdf5e9
Fix test for too many basis points create NFT error.
redpartyhat Jul 8, 2021
402aaaa
Add test to ensure that previous NFT owners cannot accept bids on the…
redpartyhat Jul 9, 2021
b124fcf
Make NFT royalty test asymmetric to make sure coin and creator royalt…
redpartyhat Jul 9, 2021
3edf4cf
Add empty PKID error catching, move reflect.DeepEqual for efficiency.
redpartyhat Jul 9, 2021
f280b0f
Add check for nil block.
redpartyhat Jul 9, 2021
7422c08
Bid History and NFT Ownership Indexes (#60)
lazynina Jul 12, 2021
8e05a11
zero-bid on serial number with min bid amount nanos set
lazynina Jul 15, 2021
db9cb58
Add functions to get high and low bids for a collection of NFTs.
redpartyhat Jul 18, 2021
ec1818b
Merge branch 'rph/nifties' of github.com:bitclout/core into rph/nifties
redpartyhat Jul 18, 2021
a9ba8cf
add bid entries to view after fetching from db when getting high and …
lazynina Jul 20, 2021
7affb11
merge in main
lazynina Jul 20, 2021
0f4ff97
changes to support decrypting unlockable text
lazynina Jul 21, 2021
1a9d2a0
Add NumNFTCopiesForSale to PostEntry.
redpartyhat Jul 23, 2021
c9b8264
add GetHighAndLowBidsForNFTSerialNumber
lazynina Jul 26, 2021
b3d7b47
fix tests
lazynina Jul 26, 2021
773a24d
put all the nft ownership index logic into put/delete nft entry mappi…
lazynina Jul 26, 2021
772679b
Kill nil slice check and add ForSale-->NotForSale paranoia check.
redpartyhat Jul 26, 2021
24efcef
Fix NFT update comment.
redpartyhat Jul 26, 2021
3a57d6a
Kill unnecessary nil check.
redpartyhat Jul 26, 2021
d29c376
Add more documentation to db_utils.
redpartyhat Jul 26, 2021
875a32e
fix naming of ownership index to reflect usage of PKID instead of pub…
lazynina Jul 26, 2021
6c84f4d
fix merge conflicts after renaming ownership index
lazynina Jul 26, 2021
54b3fa7
fix comment for ownership prefix
lazynina Jul 26, 2021
4bf5993
Add overflow check and test for NFT royalties.
redpartyhat Jul 26, 2021
c54c41e
Merge in @LazyNina's NFT _prefix fix.
redpartyhat Jul 26, 2021
28e1469
Add sanity check for balanceNanons >= immatureBlockRewards.
redpartyhat Jul 26, 2021
7f657e7
save unlockable text when connecting update NFT
lazynina Jul 26, 2021
b82d244
Merge branch 'rph/nifties' of github.com:bitclout/core into rph/nifties
lazynina Jul 26, 2021
52d34a5
Check that bidder utxos specified are actually from the bidder.
redpartyhat Jul 27, 2021
ccb9bc8
Merge branch 'rph/nifties' of github.com:bitclout/core into rph/nifties
redpartyhat Jul 27, 2021
4eb44b1
Fix spending of bidder UTXOs and add test.
redpartyhat Jul 27, 2021
59b060c
Add sanity check for royalty amounts.
redpartyhat Jul 27, 2021
e1323b6
Add hardcore sanity check. Fix underflow check.
redpartyhat Jul 27, 2021
547d01c
Small fix for hardcore sanity check.
redpartyhat Jul 27, 2021
6c37fe1
Fix comment nits.
redpartyhat Jul 27, 2021
c96100e
Block NFT transactions on global params MaxCopiesPerNFT.
redpartyhat Jul 27, 2021
c85875b
Add paranoia sanity check for nftFee overflow.
redpartyhat Jul 27, 2021
1f676bf
Fix int64 and comment nit.
redpartyhat Jul 27, 2021
69802e1
Reverse the order that UTXOs are unadded and unspent in _disconnectAc…
redpartyhat Jul 27, 2021
363cc1e
remove testnet param updater key
lazynina Jul 27, 2021
fd6c606
fix comments
lazynina Jul 27, 2021
1b265c7
Apply suggestions from code review
lazynina Jul 27, 2021
656daf3
Improve efficiency of balance computation
diamondhands0 Jul 27, 2021
3807944
Fix genesis block edge case when computing balances
diamondhands0 Jul 27, 2021
4908ddc
Fix if else nitpick
diamondhands0 Jul 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,216 changes: 2,047 additions & 169 deletions lib/block_view.go

Large diffs are not rendered by default.

4,828 changes: 4,516 additions & 312 deletions lib/block_view_test.go

Large diffs are not rendered by default.

260 changes: 250 additions & 10 deletions lib/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import (
"container/list"
"encoding/hex"
"fmt"
"math"
"math/big"
"runtime/debug"
"sort"
"strings"
"time"
chainlib "github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcec"
"github.com/davecgh/go-spew/spew"
Expand All @@ -18,6 +12,12 @@ import (
merkletree "github.com/laser/go-merkle-tree"
"github.com/pkg/errors"
"github.com/sasha-s/go-deadlock"
"math"
"math/big"
"runtime/debug"
"sort"
"strings"
"time"
)

// blockchain.go is the work-horse for validating BitClout blocks and updating the
Expand Down Expand Up @@ -2510,6 +2510,8 @@ func (bc *Blockchain) CreateFollowTxn(
func (bc *Blockchain) CreateUpdateGlobalParamsTxn(updaterPublicKey []byte,
usdCentsPerBitcoin int64,
createProfileFeesNanos int64,
createNFTFeesNanos int64,
maxCopiesPerNFT int64,
minimumNetworkFeeNanosPerKb int64,
forbiddenPubKey []byte,
// Standard transaction fields
Expand All @@ -2519,16 +2521,22 @@ func (bc *Blockchain) CreateUpdateGlobalParamsTxn(updaterPublicKey []byte,
// Set RecloutedPostHash and IsQuotedReclout on the extra data map as necessary to track reclouting.
extraData := make(map[string][]byte)
if usdCentsPerBitcoin >= 0 {
extraData[USDCentsPerBitcoin] = UintToBuf(uint64(usdCentsPerBitcoin))
extraData[USDCentsPerBitcoinKey] = UintToBuf(uint64(usdCentsPerBitcoin))
}
if createProfileFeesNanos >= 0 {
extraData[CreateProfileFeeNanos] = UintToBuf(uint64(createProfileFeesNanos))
extraData[CreateProfileFeeNanosKey] = UintToBuf(uint64(createProfileFeesNanos))
}
if createNFTFeesNanos >= 0 {
extraData[CreateNFTFeeNanosKey] = UintToBuf(uint64(createNFTFeesNanos))
}
if maxCopiesPerNFT >= 0 {
extraData[MaxCopiesPerNFTKey] = UintToBuf(uint64(maxCopiesPerNFT))
}
if minimumNetworkFeeNanosPerKb >= 0 {
extraData[MinNetworkFeeNanosPerKB] = UintToBuf(uint64(minimumNetworkFeeNanosPerKb))
extraData[MinNetworkFeeNanosPerKBKey] = UintToBuf(uint64(minimumNetworkFeeNanosPerKb))
}
if len(forbiddenPubKey) > 0 {
extraData[ForbiddenBlockSignaturePubKey] = forbiddenPubKey
extraData[ForbiddenBlockSignaturePubKeyKey] = forbiddenPubKey
}

txn := &MsgBitCloutTxn{
Expand Down Expand Up @@ -2841,6 +2849,238 @@ func (bc *Blockchain) CreateCreatorCoinTransferTxn(
return txn, totalInput, changeAmount, fees, nil
}

func (bc *Blockchain) CreateCreateNFTTxn(
UpdaterPublicKey []byte,
NFTPostHash *BlockHash,
NumCopies uint64,
HasUnlockable bool,
IsForSale bool,
MinBidAmountNanos uint64,
NFTFee uint64,
NFTRoyaltyToCreatorBasisPoints uint64,
NFTRoyaltyToCoinBasisPoints uint64,
// Standard transaction fields
minFeeRateNanosPerKB uint64, mempool *BitCloutMempool) (
_txn *MsgBitCloutTxn, _totalInput uint64, _changeAmount uint64, _fees uint64, _err error) {

// Create a transaction containing the create NFT fields.
txn := &MsgBitCloutTxn{
PublicKey: UpdaterPublicKey,
TxnMeta: &CreateNFTMetadata{
NFTPostHash,
NumCopies,
HasUnlockable,
IsForSale,
MinBidAmountNanos,
NFTRoyaltyToCreatorBasisPoints,
NFTRoyaltyToCoinBasisPoints,
},

// We wait to compute the signature until we've added all the
// inputs and change.
}

// We directly call AddInputsAndChangeToTransactionWithSubsidy so we can pass through the NFT fee.
totalInput, _, changeAmount, fees, err :=
bc.AddInputsAndChangeToTransactionWithSubsidy(txn, minFeeRateNanosPerKB, 0, mempool, NFTFee)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "CreateCreateNFTTxn: Problem adding inputs: ")
}

// We want our transaction to have at least one input, even if it all
// goes to change. This ensures that the transaction will not be "replayable."
if len(txn.TxInputs) == 0 {
return nil, 0, 0, 0, fmt.Errorf("CreateCreateNFTTxn: CreateNFT txn " +
"must have at least one input but had zero inputs " +
"instead. Try increasing the fee rate.")
}

return txn, totalInput, changeAmount, fees, nil
}

func (bc *Blockchain) CreateNFTBidTxn(
UpdaterPublicKey []byte,
NFTPostHash *BlockHash,
SerialNumber uint64,
BidAmountNanos uint64,
// Standard transaction fields
minFeeRateNanosPerKB uint64, mempool *BitCloutMempool) (
_txn *MsgBitCloutTxn, _totalInput uint64, _changeAmount uint64, _fees uint64, _err error) {

// Create a transaction containing the NFT bid fields.
txn := &MsgBitCloutTxn{
PublicKey: UpdaterPublicKey,
TxnMeta: &NFTBidMetadata{
NFTPostHash,
SerialNumber,
BidAmountNanos,
},

// We wait to compute the signature until we've added all the
// inputs and change.
}

// Add inputs and change for a standard pay per KB transaction.
totalInput, _, changeAmount, fees, err :=
bc.AddInputsAndChangeToTransaction(txn, minFeeRateNanosPerKB, mempool)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "CreateNFTBidTxn: Problem adding inputs: ")
}

// We want our transaction to have at least one input, even if it all
// goes to change. This ensures that the transaction will not be "replayable."
if len(txn.TxInputs) == 0 {
return nil, 0, 0, 0, fmt.Errorf("CreateNFTBidTxn: NFTBid txn " +
"must have at least one input but had zero inputs " +
"instead. Try increasing the fee rate.")
}

return txn, totalInput, changeAmount, fees, nil
}

func (bc *Blockchain) CreateAcceptNFTBidTxn(
UpdaterPublicKey []byte,
NFTPostHash *BlockHash,
SerialNumber uint64,
BidderPKID *PKID,
BidAmountNanos uint64,
EncryptedUnlockableTextBytes []byte,
// Standard transaction fields
minFeeRateNanosPerKB uint64, mempool *BitCloutMempool) (
_txn *MsgBitCloutTxn, _totalInput uint64, _changeAmount uint64, _fees uint64, _err error) {

// Create a new UtxoView. If we have access to a mempool object, use it to
// get an augmented view that factors in pending transactions.
utxoView, err := NewUtxoView(bc.db, bc.params, bc.bitcoinManager)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err,
"Blockchain.CreateAcceptNFTBidTxn: Problem creating new utxo view: ")
}
if mempool != nil {
utxoView, err = mempool.GetAugmentedUniversalView()
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err,
"Blockchain.CreateAcceptNFTBidTxn: Problem getting augmented UtxoView from mempool: ")
}
}

// Get the spendable UtxoEntrys.
bidderPkBytes := utxoView.GetPublicKeyForPKID(BidderPKID)
bidderSpendableUtxos, err := bc.GetSpendableUtxosForPublicKey(bidderPkBytes, nil, utxoView)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "Blockchain.CreateAcceptNFTBidTxn: Problem getting spendable UtxoEntrys: ")
}

// Add input utxos to the transaction until we have enough total input to cover
// the amount we want to spend plus the maximum fee (or until we've exhausted
// all the utxos available).
bidderInputs := []*BitCloutInput{}
totalBidderInput := uint64(0)
for _, utxoEntry := range bidderSpendableUtxos {

// If the amount of input we have isn't enough to cover the bid amount, add an input and continue.
if totalBidderInput < BidAmountNanos {
bidderInputs = append(bidderInputs, (*BitCloutInput)(utxoEntry.UtxoKey))

amountToAdd := utxoEntry.AmountNanos
// For Bitcoin burns, we subtract a tiny amount of slippage to the amount we can
// spend. This makes reorderings more forgiving.
if utxoEntry.UtxoType == UtxoTypeBitcoinBurn {
amountToAdd = uint64(float64(amountToAdd) * .999)
}

totalBidderInput += amountToAdd
continue
}

// If we get here, we know we have enough input to cover the upper bound
// estimate of our amount needed so break.
break
}

// If we get here and we don't have sufficient input to cover the bid, error.
if totalBidderInput < BidAmountNanos {
return nil, 0, 0, 0, fmt.Errorf("Blockchain.CreateAcceptNFTBidTxn: Bidder has insufficient "+
"UTXOs (%d total) to cover BidAmountNanos %d: ", totalBidderInput, BidAmountNanos)
}

// Create a transaction containing the accept nft bid fields.
txn := &MsgBitCloutTxn{
PublicKey: UpdaterPublicKey,
TxnMeta: &AcceptNFTBidMetadata{
NFTPostHash,
SerialNumber,
BidderPKID,
BidAmountNanos,
EncryptedUnlockableTextBytes,
bidderInputs,
},

// We wait to compute the signature until we've added all the
// inputs and change.
}

// Add inputs and change for a standard pay per KB transaction.
totalInput, _, changeAmount, fees, err :=
bc.AddInputsAndChangeToTransaction(txn, minFeeRateNanosPerKB, mempool)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "CreateAcceptNFTBidTxn: Problem adding inputs: ")
}

// We want our transaction to have at least one input, even if it all
// goes to change. This ensures that the transaction will not be "replayable."
if len(txn.TxInputs) == 0 {
return nil, 0, 0, 0, fmt.Errorf("CreateAcceptNFTBidTxn: AcceptNFTBid txn " +
"must have at least one input but had zero inputs " +
"instead. Try increasing the fee rate.")
}

return txn, totalInput, changeAmount, fees, nil
}

func (bc *Blockchain) CreateUpdateNFTTxn(
UpdaterPublicKey []byte,
NFTPostHash *BlockHash,
SerialNumber uint64,
IsForSale bool,
MinBidAmountNanos uint64,
// Standard transaction fields
minFeeRateNanosPerKB uint64, mempool *BitCloutMempool) (
_txn *MsgBitCloutTxn, _totalInput uint64, _changeAmount uint64, _fees uint64, _err error) {

// Create a transaction containing the update NFT fields.
txn := &MsgBitCloutTxn{
PublicKey: UpdaterPublicKey,
TxnMeta: &UpdateNFTMetadata{
NFTPostHash,
SerialNumber,
IsForSale,
MinBidAmountNanos,
},

// We wait to compute the signature until we've added all the
// inputs and change.
}

// Add inputs and change for a standard pay per KB transaction.
totalInput, spendAmount, changeAmount, fees, err :=
bc.AddInputsAndChangeToTransaction(txn, minFeeRateNanosPerKB, mempool)
if err != nil {
return nil, 0, 0, 0, errors.Wrapf(err, "CreateUpdateNFTTxn: Problem adding inputs: ")
}
_ = spendAmount

// We want our transaction to have at least one input, even if it all
// goes to change. This ensures that the transaction will not be "replayable."
if len(txn.TxInputs) == 0 {
return nil, 0, 0, 0, fmt.Errorf("CreateUpdateNFTTxn: AcceptNFTBid txn " +
"must have at least one input but had zero inputs " +
"instead. Try increasing the fee rate.")
}

return txn, totalInput, changeAmount, fees, nil
}

// Each diamond level is worth a fixed amount of BitClout. These amounts can be changed
// in the future by simply returning a new set of values after a particular block height.
func GetBitCloutNanosDiamondLevelMapAtBlockHeight(
Expand Down
33 changes: 32 additions & 1 deletion lib/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,40 @@ func _getBalance(t *testing.T, chain *Blockchain, mempool *BitCloutMempool, pkSt
for _, utxoEntry := range utxoEntriesFound {
balanceForUserNanos += utxoEntry.AmountNanos
}

utxoView, err := NewUtxoView(chain.db, chain.params, chain.bitcoinManager)
require.NoError(t, err)
if mempool != nil {
utxoView, err = mempool.GetAugmentedUniversalView()
require.NoError(t, err)
}

balanceNanos, err := utxoView.GetSpendableBitcloutBalanceNanosForPublicKey(
pkBytes, chain.headerTip().Height)
require.NoError(t, err)

// DO NOT REMOVE: This is used to test the similarity of UTXOs vs. the pubkey balance index.
require.Equal(t, balanceForUserNanos, balanceNanos)

return balanceForUserNanos
}

func _getCreatorCoinInfo(t *testing.T, db *badger.DB, params *BitCloutParams, pkStr string,
) (_bitCloutLocked uint64, _coinsInCirculation uint64) {
pkBytes, _, err := Base58CheckDecode(pkStr)
require.NoError(t, err)

utxoView, _ := NewUtxoView(db, params, nil)

// Profile fields
creatorProfile := utxoView.GetProfileEntryForPublicKey(pkBytes)
if creatorProfile == nil {
return 0, 0
}

return creatorProfile.BitCloutLockedNanos, creatorProfile.CoinsInCirculationNanos
}

func _getBalanceWithView(t *testing.T, utxoView *UtxoView, pkStr string) uint64 {
pkBytes, _, err := Base58CheckDecode(pkStr)
require.NoError(t, err)
Expand Down Expand Up @@ -1495,7 +1526,7 @@ func TestForbiddenBlockSignaturePubKey(t *testing.T) {
blockSignerPkBytes, _, err := Base58CheckDecode(blockSignerPk)
require.NoError(err)
txn, _, _, _, err := chain.CreateUpdateGlobalParamsTxn(
senderPkBytes, -1, -1, -1, blockSignerPkBytes, 100 /*feeRateNanosPerKB*/, nil)
senderPkBytes, -1, -1, -1, -1, -1, blockSignerPkBytes, 100 /*feeRateNanosPerKB*/, nil)
require.NoError(err)

// Mine a few blocks to give the senderPkString some money.
Expand Down
Loading