diff --git a/cmd/web/web.go b/cmd/web/web.go index 78b4475..0892673 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -736,7 +736,7 @@ func main() { if buf, err := hex.DecodeString(string(s)); err == nil { - raw, _ = sidechain.NewShareFromExportedBytes(buf) + raw, _ = sidechain.NewShareFromExportedBytes(buf, sidechain.NetworkMainnet) } } diff --git a/monero/address/address_test.go b/monero/address/address_test.go index ba83e4d..d390edd 100644 --- a/monero/address/address_test.go +++ b/monero/address/address_test.go @@ -41,7 +41,7 @@ var previousId, _ = types.HashFromString("d59abce89ce8131eba025988d1ea372937f2ac var detTxPriv, _ = types.HashFromString("10f3941fd50ca266d3350984004a804c887c36ec11620080fe0b7c4a2a208605") func TestDeterministic(t *testing.T) { - detTx := types.HashFromBytes(GetDeterministicTransactionPrivateKey(testAddress2, previousId).AsSlice()) + detTx := types.HashFromBytes(GetDeterministicTransactionPrivateKey(types.Hash(testAddress2.SpendPublicKey().AsBytes()), previousId).AsSlice()) if detTx != detTxPriv { t.Fatal() } diff --git a/monero/address/crypto.go b/monero/address/crypto.go index 6770898..ae3929a 100644 --- a/monero/address/crypto.go +++ b/monero/address/crypto.go @@ -10,8 +10,8 @@ import ( "strings" ) -func GetDeterministicTransactionPrivateKey(a Interface, prevId types.Hash) crypto.PrivateKey { - return p2poolcrypto.GetDeterministicTransactionPrivateKey(a.SpendPublicKey(), prevId) +func GetDeterministicTransactionPrivateKey(seed types.Hash, prevId types.Hash) crypto.PrivateKey { + return p2poolcrypto.GetDeterministicTransactionPrivateKey(seed, prevId) } func GetPublicKeyForSharedData(a Interface, sharedData crypto.PrivateKey) crypto.PublicKey { diff --git a/monero/block/block.go b/monero/block/block.go index 2262dee..72c4944 100644 --- a/monero/block/block.go +++ b/monero/block/block.go @@ -197,7 +197,9 @@ func (b *Block) HeaderBlob() []byte { return buf } +// SideChainHashingBlob Same as MarshalBinary but with nonce set to 0 func (b *Block) SideChainHashingBlob() (buf []byte, err error) { + var txBuf []byte if txBuf, err = b.Coinbase.SideChainHashingBlob(); err != nil { return nil, err diff --git a/p2pool/api/api.go b/p2pool/api/api.go index f83aede..eeb4825 100644 --- a/p2pool/api/api.go +++ b/p2pool/api/api.go @@ -92,7 +92,7 @@ func (a *Api) GetFailedRawBlock(id types.Hash) (b *sidechain.PoolBlock, err erro } else { data := make([]byte, len(buf)/2) _, _ = hex.Decode(data, buf) - return sidechain.NewShareFromExportedBytes(data) + return sidechain.NewShareFromExportedBytes(data, sidechain.NetworkMainnet) } } @@ -111,7 +111,7 @@ func (a *Api) GetRawBlock(id types.Hash) (b *sidechain.PoolBlock, err error) { } else { data := make([]byte, len(buf)/2) _, _ = hex.Decode(data, buf) - return sidechain.NewShareFromExportedBytes(data) + return sidechain.NewShareFromExportedBytes(data, sidechain.NetworkMainnet) } } diff --git a/p2pool/crypto/crypto.go b/p2pool/crypto/crypto.go index c954f8c..0facba4 100644 --- a/p2pool/crypto/crypto.go +++ b/p2pool/crypto/crypto.go @@ -6,7 +6,16 @@ import ( "unsafe" ) -func GetDeterministicTransactionPrivateKey(spendPublicKey crypto.PublicKey, previousMoneroId types.Hash) crypto.PrivateKey { +func CalculateTransactionPrivateKeySeed(main, side []byte) types.Hash { + return crypto.PooledKeccak256( + // domain + []byte("tx_key_seed"), //TODO: check for null termination + main, + side, + ) +} + +func GetDeterministicTransactionPrivateKey(seed types.Hash, previousMoneroId types.Hash) crypto.PrivateKey { /* Current deterministic key issues * This Deterministic private key changes too ofter, and does not fit full purpose (prevent knowledge of private keys on Coinbase without observing of sidechains). @@ -20,11 +29,11 @@ func GetDeterministicTransactionPrivateKey(spendPublicKey crypto.PublicKey, prev */ - var entropy [13 + crypto.PublicKeySize + types.HashSize + (int(unsafe.Sizeof(uint32(0))) /*pre-allocate uint32 for counter*/)]byte + var entropy [13 + types.HashSize + types.HashSize + (int(unsafe.Sizeof(uint32(0))) /*pre-allocate uint32 for counter*/)]byte copy(entropy[:], []byte("tx_secret_key")) //domain - copy(entropy[13:], spendPublicKey.AsSlice()) - copy(entropy[13+crypto.PublicKeySize:], previousMoneroId[:]) - return crypto.PrivateKeyFromScalar(crypto.DeterministicScalar(entropy[:13+crypto.PublicKeySize+types.HashSize])) + copy(entropy[13:], seed[:]) + copy(entropy[13+types.HashSize:], previousMoneroId[:]) + return crypto.PrivateKeyFromScalar(crypto.DeterministicScalar(entropy[:13+types.HashSize+types.HashSize])) /* diff --git a/p2pool/mainchain/mainchain.go b/p2pool/mainchain/mainchain.go index 6e9b549..44b6cb6 100644 --- a/p2pool/mainchain/mainchain.go +++ b/p2pool/mainchain/mainchain.go @@ -182,7 +182,7 @@ func (c *MainChain) HandleMainBlock(b *mainblock.Block) { } // HandleChainMain -// deprecated +// Deprecated func (c *MainChain) HandleChainMain(mainData *sidechain.ChainMain, extra []byte) { func() { c.lock.Lock() diff --git a/p2pool/p2p/client.go b/p2pool/p2p/client.go index 1bb5bc4..3af3908 100644 --- a/p2pool/p2p/client.go +++ b/p2pool/p2p/client.go @@ -367,6 +367,7 @@ func (c *Client) OnConnection() { case MessageBlockResponse: block := &sidechain.PoolBlock{ LocalTimestamp: uint64(time.Now().Unix()), + NetworkType: c.Owner.Consensus().NetworkType, } expectedBlockId, ok := c.getNextBlockRequest() @@ -424,6 +425,7 @@ func (c *Client) OnConnection() { case MessageBlockBroadcast, MessageBlockBroadcastCompact: block := &sidechain.PoolBlock{ LocalTimestamp: uint64(time.Now().Unix()), + NetworkType: c.Owner.Consensus().NetworkType, } var blockSize uint32 if err := binary.Read(c, binary.LittleEndian, &blockSize); err != nil { diff --git a/p2pool/sidechain/blobcache.go b/p2pool/sidechain/blobcache.go index 45b52fd..dcf95d0 100644 --- a/p2pool/sidechain/blobcache.go +++ b/p2pool/sidechain/blobcache.go @@ -13,10 +13,10 @@ import ( const BlockSaveEpochSize = 32 const ( - BlockSaveOptionTemplate = 1 << 0 - BlockSaveOptionDeterministicPrivateKey = 1 << 1 - BlockSaveOptionDeterministicBlobs = 1 << 2 - BlockSaveOptionUncles = 1 << 3 + BlockSaveOptionTemplate = 1 << 0 + //BlockSaveOptionDeterministicPrivateKey = 1 << 1 + BlockSaveOptionDeterministicBlobs = 1 << 2 + BlockSaveOptionUncles = 1 << 3 BlockSaveFieldSizeInBits = 8 @@ -61,7 +61,7 @@ func (c *SideChain) saveBlock(block *PoolBlock) { defer c.sidechainLock.RUnlock() //only store keys when not deterministic - isDeterministicPrivateKey := c.isPoolBlockTransactionKeyIsDeterministic(block) + //isDeterministicPrivateKey := c.isPoolBlockTransactionKeyIsDeterministic(block) calculatedOutputs := c.calculateOutputs(block) calcBlob, _ := calculatedOutputs.MarshalBinary() @@ -80,9 +80,9 @@ func (c *SideChain) saveBlock(block *PoolBlock) { var blockFlags uint64 - if isDeterministicPrivateKey { + /*if isDeterministicPrivateKey { blockFlags |= BlockSaveOptionDeterministicPrivateKey - } + }*/ if !storeBlob { blockFlags |= BlockSaveOptionDeterministicBlobs @@ -164,8 +164,8 @@ func (c *SideChain) saveBlock(block *PoolBlock) { } // private key, if needed - if (blockFlags & BlockSaveOptionDeterministicPrivateKey) == 0 { - blob = append(blob, block.Side.CoinbasePrivateKey[:]...) + if true /*(blockFlags & BlockSaveOptionDeterministicPrivateKey) == 0*/ { + blob = append(blob, block.Side.CoinbasePrivateKeySeed[:]...) //public may be needed on invalid - TODO check //blob = append(blob, block.CoinbaseExtra(SideCoinbasePublicKey)...) } diff --git a/p2pool/sidechain/cache.go b/p2pool/sidechain/cache.go index 8682cca..206519b 100644 --- a/p2pool/sidechain/cache.go +++ b/p2pool/sidechain/cache.go @@ -61,14 +61,14 @@ func (d *DerivationCache) GetEphemeralPublicKey(a address.Interface, txKeySlice } } -func (d *DerivationCache) GetDeterministicTransactionKey(a address.Interface, prevId types.Hash) *crypto.KeyPair { +func (d *DerivationCache) GetDeterministicTransactionKey(seed types.Hash, prevId types.Hash) *crypto.KeyPair { var key deterministicTransactionCacheKey - copy(key[:], a.SpendPublicKey().AsSlice()) + copy(key[:], seed[:]) copy(key[types.HashSize:], prevId[:]) deterministicKeyCache := d.deterministicKeyCache.Load() if kp := deterministicKeyCache.Get(key); kp == nil { - data := crypto.NewKeyPairFromPrivate(address.GetDeterministicTransactionPrivateKey(a, prevId)) + data := crypto.NewKeyPairFromPrivate(address.GetDeterministicTransactionPrivateKey(seed, prevId)) deterministicKeyCache.Set(key, data) return data } else { diff --git a/p2pool/sidechain/consensus.go b/p2pool/sidechain/consensus.go index 38c5809..c69ecc7 100644 --- a/p2pool/sidechain/consensus.go +++ b/p2pool/sidechain/consensus.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "git.gammaspectra.live/P2Pool/p2pool-observer/monero" - mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx" "git.gammaspectra.live/P2Pool/p2pool-observer/types" @@ -131,7 +130,7 @@ func (c *Consensus) verify() bool { return false } - if c.MinimumDifficulty < SmallestMinimumDifficulty || c.MinimumDifficulty > LargestMinimumDifficulty { + if c.NetworkType == NetworkMainnet && c.MinimumDifficulty < SmallestMinimumDifficulty || c.MinimumDifficulty > LargestMinimumDifficulty { return false } @@ -152,15 +151,16 @@ func (c *Consensus) verify() bool { return true } -func (c *Consensus) CalculateSideTemplateId(main *mainblock.Block, side *SideData) types.Hash { +func (c *Consensus) CalculateSideTemplateId(share *PoolBlock) types.Hash { - mainData, _ := main.SideChainHashingBlob() - sideData, _ := side.MarshalBinary() + mainData, _ := share.Main.SideChainHashingBlob() + sideData, _ := share.Side.MarshalBinary(share.ShareVersion()) return c.CalculateSideChainIdFromBlobs(mainData, sideData) } func (c *Consensus) CalculateSideChainIdFromBlobs(mainBlob, sideBlob []byte) types.Hash { + //TODO: handle extra nonce return crypto.PooledKeccak256(mainBlob, sideBlob, c.id[:]) } diff --git a/p2pool/sidechain/poolblock.go b/p2pool/sidechain/poolblock.go index a8eaff1..288cfa2 100644 --- a/p2pool/sidechain/poolblock.go +++ b/p2pool/sidechain/poolblock.go @@ -9,8 +9,10 @@ import ( mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction" + p2poolcrypto "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/crypto" "git.gammaspectra.live/P2Pool/p2pool-observer/types" "io" + "log" "sync" "sync/atomic" "unsafe" @@ -27,6 +29,17 @@ const ( SideTemplateId = transaction.TxExtraTagMergeMining ) +type ShareVersion int + +const ( + ShareVersion_None ShareVersion = 0 + ShareVersion_V1 ShareVersion = 1 + ShareVersion_V2 ShareVersion = 2 +) + +const ShareVersion_V2MainNetTimestamp uint64 = 1679173200 // 2023-03-18 21:00 UTC +const ShareVersion_V2TestNetTimestamp uint64 = 1674507600 // 2023-01-23 21:00 UTC + type PoolBlock struct { Main mainblock.Block @@ -41,12 +54,16 @@ type PoolBlock struct { WantBroadcast atomic.Bool Broadcasted atomic.Bool + NetworkType NetworkType LocalTimestamp uint64 } // NewShareFromExportedBytes TODO deprecate this in favor of standard serialized shares -func NewShareFromExportedBytes(buf []byte) (*PoolBlock, error) { - b := &PoolBlock{} +// Deprecated +func NewShareFromExportedBytes(buf []byte, networkType NetworkType) (*PoolBlock, error) { + b := &PoolBlock{ + NetworkType: networkType, + } if len(buf) < 32 { return nil, errors.New("invalid block data") @@ -141,7 +158,7 @@ func NewShareFromExportedBytes(buf []byte) (*PoolBlock, error) { return nil, err } - if err = b.Side.UnmarshalBinary(sideData); err != nil { + if err = b.Side.UnmarshalBinary(sideData, b.ShareVersion()); err != nil { return nil, err } @@ -150,6 +167,37 @@ func NewShareFromExportedBytes(buf []byte) (*PoolBlock, error) { return b, nil } +func (b *PoolBlock) ShareVersion() ShareVersion { + // P2Pool forks to v2 at 2023-03-18 21:00 UTC + // Different miners can have different timestamps, + // so a temporary mix of v1 and v2 blocks is allowed + switch b.NetworkType { + case NetworkInvalid: + log.Panicf("invalid network type for determining share version") + case NetworkMainnet: + if b.Main.Timestamp >= ShareVersion_V2MainNetTimestamp { + return ShareVersion_V2 + } + case NetworkTestnet: + if b.Main.Timestamp >= ShareVersion_V2TestNetTimestamp { + return ShareVersion_V2 + } + case NetworkStagenet: + return ShareVersion_V2 + } + if b.Main.Timestamp >= ShareVersion_V2MainNetTimestamp { + return ShareVersion_V2 + } + return ShareVersion_V1 +} + +func (b *PoolBlock) ShareVersionSignaling() ShareVersion { + if b.ShareVersion() == ShareVersion_V1 && ((binary.LittleEndian.Uint32(b.CoinbaseExtra(SideExtraNonce)))&0xFF100000 == 0xFF000000) { + return ShareVersion_V2 + } + return ShareVersion_None +} + func (b *PoolBlock) CoinbaseExtra(tag CoinbaseExtraTag) []byte { switch tag { case SideExtraNonce: @@ -271,7 +319,7 @@ func (b *PoolBlock) SideTemplateId(consensus *Consensus) types.Hash { b.cache.lock.Lock() defer b.cache.lock.Unlock() if b.cache.templateId == types.ZeroHash { //check again for race - b.cache.templateId = consensus.CalculateSideTemplateId(&b.Main, &b.Side) + b.cache.templateId = consensus.CalculateSideTemplateId(b) } return b.cache.templateId } @@ -311,7 +359,7 @@ func (b *PoolBlock) UnmarshalBinary(data []byte) error { func (b *PoolBlock) MarshalBinary() ([]byte, error) { if mainData, err := b.Main.MarshalBinary(); err != nil { return nil, err - } else if sideData, err := b.Side.MarshalBinary(); err != nil { + } else if sideData, err := b.Side.MarshalBinary(b.ShareVersion()); err != nil { return nil, err } else { data := make([]byte, 0, len(mainData)+len(sideData)) @@ -324,7 +372,7 @@ func (b *PoolBlock) MarshalBinary() ([]byte, error) { func (b *PoolBlock) MarshalBinaryFlags(pruned, compact bool) ([]byte, error) { if mainData, err := b.Main.MarshalBinaryFlags(pruned, compact); err != nil { return nil, err - } else if sideData, err := b.Side.MarshalBinary(); err != nil { + } else if sideData, err := b.Side.MarshalBinary(b.ShareVersion()); err != nil { return nil, err } else { data := make([]byte, 0, len(mainData)+len(sideData)) @@ -339,7 +387,7 @@ func (b *PoolBlock) FromReader(reader readerAndByteReader) (err error) { return err } - if err = b.Side.FromReader(reader); err != nil { + if err = b.Side.FromReader(reader, b.ShareVersion()); err != nil { return err } @@ -352,7 +400,7 @@ func (b *PoolBlock) FromCompactReader(reader readerAndByteReader) (err error) { return err } - if err = b.Side.FromReader(reader); err != nil { + if err = b.Side.FromReader(reader, b.ShareVersion()); err != nil { return err } @@ -387,6 +435,26 @@ func (b *PoolBlock) IsProofHigherThanDifficultyWithError(f mainblock.GetSeedByHe } } +func (b *PoolBlock) GetPrivateKeySeed() types.Hash { + if b.ShareVersion() > ShareVersion_V1 { + return b.Side.CoinbasePrivateKeySeed + } + return types.Hash(b.Side.PublicSpendKey.AsBytes()) +} + +func (b *PoolBlock) CalculateTransactionPrivateKeySeed() types.Hash { + if b.ShareVersion() > ShareVersion_V1 { + mainData, _ := b.Main.SideChainHashingBlob() + sideData, _ := b.Side.MarshalBinary(b.ShareVersion()) + return p2poolcrypto.CalculateTransactionPrivateKeySeed( + mainData, + sideData, + ) + } + + return types.Hash(b.Side.PublicSpendKey.AsBytes()) +} + func (b *PoolBlock) GetAddress() *address.PackedAddress { a := address.NewPackedAddressFromBytes(b.Side.PublicSpendKey, b.Side.PublicViewKey) return &a diff --git a/p2pool/sidechain/poolblock_test.go b/p2pool/sidechain/poolblock_test.go index bc03c31..1cd6815 100644 --- a/p2pool/sidechain/poolblock_test.go +++ b/p2pool/sidechain/poolblock_test.go @@ -32,7 +32,7 @@ func TestPoolBlockDecode(t *testing.T) { t.Fatal(err) } - block, err := NewShareFromExportedBytes(contents) + block, err := NewShareFromExportedBytes(contents, NetworkMainnet) if err != nil { t.Fatal(err) } @@ -57,14 +57,14 @@ func TestPoolBlockDecode(t *testing.T) { b2, _ := block.Main.MarshalBinary() side2 := &SideData{} - if err = side2.UnmarshalBinary(b2); err != nil { + if err = side2.UnmarshalBinary(b2, block.ShareVersion()); err != nil { t.Fatal(err) } t.Log(block.SideTemplateId(ConsensusDefault).String()) t.Log(block.Side.CoinbasePrivateKey.String()) - t.Log(address.GetDeterministicTransactionPrivateKey(block.GetAddress(), block.Main.PreviousId).String()) + t.Log(address.GetDeterministicTransactionPrivateKey(block.GetPrivateKeySeed(), block.Main.PreviousId).String()) txId := block.Main.Coinbase.Id() diff --git a/p2pool/sidechain/sidechain.go b/p2pool/sidechain/sidechain.go index f858c21..bf34bf6 100644 --- a/p2pool/sidechain/sidechain.go +++ b/p2pool/sidechain/sidechain.go @@ -2,18 +2,21 @@ package sidechain import ( "bytes" + "encoding/binary" "errors" "fmt" "git.gammaspectra.live/P2Pool/p2pool-observer/monero" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/client" + "git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx" "git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction" "git.gammaspectra.live/P2Pool/p2pool-observer/types" "git.gammaspectra.live/P2Pool/p2pool-observer/utils" "golang.org/x/exp/slices" "log" + "lukechampine.com/uint128" "math" "sync" "sync/atomic" @@ -151,8 +154,11 @@ func (c *SideChain) fillPoolBlockTransactionParentIndices(block *PoolBlock) { } func (c *SideChain) isPoolBlockTransactionKeyIsDeterministic(block *PoolBlock) bool { - kP := c.derivationCache.GetDeterministicTransactionKey(block.GetAddress(), block.Main.PreviousId) - return bytes.Compare(block.CoinbaseExtra(SideCoinbasePublicKey), kP.PublicKey.AsSlice()) == 0 && bytes.Compare(block.Side.CoinbasePrivateKey[:], kP.PrivateKey.AsSlice()) == 0 + kP := c.derivationCache.GetDeterministicTransactionKey(block.GetPrivateKeySeed(), block.Main.PreviousId) + if block.ShareVersion() > ShareVersion_V1 { + block.Side.CoinbasePrivateKey = kP.PrivateKey.AsBytes() + } + return bytes.Compare(block.CoinbaseExtra(SideCoinbasePublicKey), kP.PublicKey.AsSlice()) == 0 && bytes.Compare(kP.PrivateKey.AsSlice(), block.Side.CoinbasePrivateKey[:]) == 0 } func (c *SideChain) getSeedByHeightFunc() mainblock.GetSeedByHeightFunc { @@ -192,7 +198,7 @@ func (c *SideChain) AddPoolBlockExternal(block *PoolBlock) (missingBlocks []type } } - templateId := c.Consensus().CalculateSideTemplateId(&block.Main, &block.Side) + templateId := c.Consensus().CalculateSideTemplateId(block) if templateId != block.SideTemplateId(c.Consensus()) { return nil, fmt.Errorf("invalid template id %s, expected %s", templateId.String(), block.SideTemplateId(c.Consensus()).String()) } @@ -384,7 +390,8 @@ func (c *SideChain) verifyBlock(block *PoolBlock) (verification error, invalid e if block.Side.Parent != types.ZeroHash || len(block.Side.Uncles) != 0 || block.Side.Difficulty.Cmp64(c.Consensus().MinimumDifficulty) != 0 || - block.Side.CumulativeDifficulty.Cmp64(c.Consensus().MinimumDifficulty) != 0 { + block.Side.CumulativeDifficulty.Cmp64(c.Consensus().MinimumDifficulty) != 0 || + (block.ShareVersion() > ShareVersion_V1 && block.Side.CoinbasePrivateKeySeed == types.ZeroHash) { return nil, errors.New("genesis block has invalid parameters") } //this does not verify coinbase outputs, but that's fine @@ -417,6 +424,16 @@ func (c *SideChain) verifyBlock(block *PoolBlock) (verification error, invalid e return nil, errors.New("parent is invalid") } + if block.ShareVersion() > ShareVersion_V1 { + expectedSeed := parent.Side.CoinbasePrivateKeySeed + if parent.Main.PreviousId != block.Main.PreviousId { + expectedSeed = parent.CalculateTransactionPrivateKeySeed() + } + if block.Side.CoinbasePrivateKeySeed != expectedSeed { + return nil, fmt.Errorf("invalid tx key seed: expected %s, got %s", expectedSeed.String(), block.Side.CoinbasePrivateKeySeed.String()) + } + } + expectedHeight := parent.Side.Height + 1 if expectedHeight != block.Side.Height { return nil, fmt.Errorf("wrong height, expected %d", expectedHeight) @@ -524,7 +541,7 @@ func (c *SideChain) verifyBlock(block *PoolBlock) (verification error, invalid e return nil, fmt.Errorf("wrong difficulty, got %s, expected %s", block.Side.Difficulty.StringNumeric(), diff.StringNumeric()) } - if c.sharesCache = c.getShares(block, c.sharesCache); len(c.sharesCache) == 0 { + if c.sharesCache, _ = c.getShares(block, c.sharesCache); len(c.sharesCache) == 0 { return nil, errors.New("could not get outputs") } else if len(c.sharesCache) != len(block.Main.Coinbase.Outputs) { return nil, fmt.Errorf("invalid number of outputs, got %d, expected %d", len(block.Main.Coinbase.Outputs), len(c.sharesCache)) @@ -641,6 +658,11 @@ func (c *SideChain) updateChainTip(block *PoolBlock) { tip := c.GetChainTip() + if block == tip { + log.Printf("[SideChain] Trying to update chain tip to the same block again. Ignoring it.") + return + } + if isLongerChain, isAlternative := c.isLongerChain(tip, block); isLongerChain { if diff := c.getDifficulty(block); diff != types.ZeroDifficulty { c.chainTip.Store(block) @@ -777,7 +799,7 @@ func (c *SideChain) getOutputs(block *PoolBlock) transaction.Outputs { func (c *SideChain) calculateOutputs(block *PoolBlock) transaction.Outputs { //TODO: buffer - tmpShares := c.getShares(block, make(Shares, 0, c.Consensus().ChainWindowSize*2)) + tmpShares, _ := c.getShares(block, make(Shares, 0, c.Consensus().ChainWindowSize*2)) tmpRewards := c.SplitReward(block.Main.Coinbase.TotalReward, tmpShares) if tmpShares == nil || tmpRewards == nil || len(tmpRewards) != len(tmpShares) { @@ -844,19 +866,54 @@ func (c *SideChain) SplitReward(reward uint64, shares Shares) (rewards []uint64) return rewards } -func (c *SideChain) getShares(tip *PoolBlock, preAllocatedShares Shares) Shares { +func (c *SideChain) getShares(tip *PoolBlock, preAllocatedShares Shares) (shares Shares, bottomHeight uint64) { var blockDepth uint64 cur := tip + var mainchainDiff types.Difficulty + + if tip.Side.Parent != types.ZeroHash { + mainchainDiff = c.server.GetDifficultyByHeight(tip.Main.Coinbase.GenHeight) + if mainchainDiff == types.ZeroDifficulty { + log.Printf("[SideChain] get_shares: couldn't get mainchain difficulty for height = %d", tip.Main.Coinbase.GenHeight) + return nil, 0 + } + } + + // Dynamic PPLNS window starting from v2 + // Limit PPLNS weight to 2x of the Monero difficulty (max 2 blocks per PPLNS window on average) + sidechainVersion := tip.ShareVersion() + + maxPplnsWeight := types.MaxDifficulty + + if sidechainVersion > ShareVersion_V1 { + maxPplnsWeight = mainchainDiff.Mul64(2) + } + + var pplnsWeight types.Difficulty + + sharesSet := make(map[address.PackedAddress]*Share, c.Consensus().ChainWindowSize*2) + + insertSet := func(weight types.Difficulty, a *address.PackedAddress) { + if _, ok := sharesSet[*a]; ok { + sharesSet[*a].Weight = sharesSet[*a].Weight.Add(weight) + } else { + sharesSet[*a] = &Share{ + Weight: weight, + Address: *a, + } + } + } + index := 0 l := len(preAllocatedShares) - insert := func(weight types.Difficulty, a *address.PackedAddress) { + insertPreAllocated := func(share *Share) { if index < l { - preAllocatedShares[index].Weight, preAllocatedShares[index].Address = weight, *a + preAllocatedShares[index].Weight, preAllocatedShares[index].Address = share.Weight, share.Address } else { - preAllocatedShares = append(preAllocatedShares, &Share{Weight: weight, Address: *a}) + preAllocatedShares = append(preAllocatedShares, share) } index++ } @@ -867,7 +924,7 @@ func (c *SideChain) getShares(tip *PoolBlock, preAllocatedShares Shares) Shares for _, uncleId := range cur.Side.Uncles { if uncle := c.getPoolBlockByTemplateId(uncleId); uncle == nil { //cannot find uncles - return nil + return nil, 0 } else { // Skip uncles which are already out of PPLNS window if (tip.Side.Height - uncle.Side.Height) >= c.Consensus().ChainWindowSize { @@ -875,15 +932,31 @@ func (c *SideChain) getShares(tip *PoolBlock, preAllocatedShares Shares) Shares } // Take some % of uncle's weight into this share - product := uncle.Side.Difficulty.Mul64(c.Consensus().UnclePenalty) - unclePenalty := product.Div64(100) + unclePenalty := uncle.Side.Difficulty.Mul64(c.Consensus().UnclePenalty).Div64(100) + uncleWeight := uncle.Side.Difficulty.Sub(unclePenalty) + newPplnsWeight := pplnsWeight.Add(uncleWeight) + + // Skip uncles that push PPLNS weight above the limit + if newPplnsWeight.Cmp(maxPplnsWeight) > 0 { + continue + } curWeight = curWeight.Add(unclePenalty) - insert(uncle.Side.Difficulty.Sub(unclePenalty), uncle.GetAddress()) + insertSet(uncleWeight, uncle.GetAddress()) + + pplnsWeight = newPplnsWeight } } - insert(curWeight, cur.GetAddress()) + // Always add non-uncle shares even if PPLNS weight goes above the limit + insertSet(curWeight, cur.GetAddress()) + + pplnsWeight = pplnsWeight.Add(curWeight) + + // One non-uncle share can go above the limit, but it will also guarantee that "shares" is never empty + if pplnsWeight.Cmp(maxPplnsWeight) > 0 { + break + } blockDepth++ @@ -899,29 +972,39 @@ func (c *SideChain) getShares(tip *PoolBlock, preAllocatedShares Shares) Shares cur = c.getParent(cur) if cur == nil { - return nil + return nil, 0 } } - shares := preAllocatedShares[:index] + bottomHeight = cur.Side.Height + + for _, share := range sharesSet { + insertPreAllocated(share) + } + + shares = preAllocatedShares[:index] // Combine shares with the same wallet addresses slices.SortFunc(shares, func(a *Share, b *Share) bool { return a.Address.Compare(&b.Address) < 0 }) - k := 0 - for i := 1; i < len(shares); i++ { - if shares[i].Address.Compare(&shares[k].Address) == 0 { - shares[k].Weight = shares[k].Weight.Add(shares[i].Weight) - } else { - k++ - shares[k].Address = shares[i].Address - shares[k].Weight = shares[i].Weight + n := len(shares) + + //Shuffle shares + if sidechainVersion > ShareVersion_V1 && n > 1 { + h := crypto.PooledKeccak256(tip.Side.CoinbasePrivateKeySeed[:]) + seed := binary.LittleEndian.Uint64(h[:]) + + for i := 0; i < (n - 1); i++ { + seed = utils.XorShift64Star(seed) + k := int(uint128.From64(seed).Mul64(uint64(n - 1)).Hi) + //swap + shares[i], shares[i+k] = shares[i+k], shares[i] } } - return shares[:k+1] + return shares, bottomHeight } type DifficultyData struct { diff --git a/p2pool/sidechain/sidedata.go b/p2pool/sidechain/sidedata.go index aad593e..60f6187 100644 --- a/p2pool/sidechain/sidedata.go +++ b/p2pool/sidechain/sidedata.go @@ -9,14 +9,24 @@ import ( ) type SideData struct { - PublicSpendKey crypto.PublicKeyBytes - PublicViewKey crypto.PublicKeyBytes + PublicSpendKey crypto.PublicKeyBytes + PublicViewKey crypto.PublicKeyBytes + CoinbasePrivateKeySeed types.Hash + // CoinbasePrivateKey filled either on decoding, or side chain filling CoinbasePrivateKey crypto.PrivateKeyBytes Parent types.Hash Uncles []types.Hash Height uint64 Difficulty types.Difficulty CumulativeDifficulty types.Difficulty + + // ExtraBuffer available in ShareVersion_2 and above + ExtraBuffer struct { + SoftwareId uint32 + Version uint32 + RandomNumber uint32 + SideChainExtraNonce uint32 + } } type readerAndByteReader interface { @@ -24,11 +34,11 @@ type readerAndByteReader interface { io.ByteReader } -func (b *SideData) MarshalBinary() (buf []byte, err error) { - buf = make([]byte, 0, types.HashSize+types.HashSize+types.HashSize+types.HashSize+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64) +func (b *SideData) MarshalBinary(version ShareVersion) (buf []byte, err error) { + buf = make([]byte, 0, types.HashSize+types.HashSize+types.HashSize+types.HashSize+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+binary.MaxVarintLen64+4*4) buf = append(buf, b.PublicSpendKey[:]...) buf = append(buf, b.PublicViewKey[:]...) - buf = append(buf, b.CoinbasePrivateKey[:]...) + buf = append(buf, b.CoinbasePrivateKeySeed[:]...) buf = append(buf, b.Parent[:]...) buf = binary.AppendUvarint(buf, uint64(len(b.Uncles))) for _, uId := range b.Uncles { @@ -39,11 +49,17 @@ func (b *SideData) MarshalBinary() (buf []byte, err error) { buf = binary.AppendUvarint(buf, b.Difficulty.Hi) buf = binary.AppendUvarint(buf, b.CumulativeDifficulty.Lo) buf = binary.AppendUvarint(buf, b.CumulativeDifficulty.Hi) + if version > ShareVersion_V1 { + buf = binary.LittleEndian.AppendUint32(buf, b.ExtraBuffer.SoftwareId) + buf = binary.LittleEndian.AppendUint32(buf, b.ExtraBuffer.Version) + buf = binary.LittleEndian.AppendUint32(buf, b.ExtraBuffer.RandomNumber) + buf = binary.LittleEndian.AppendUint32(buf, b.ExtraBuffer.SideChainExtraNonce) + } return buf, nil } -func (b *SideData) FromReader(reader readerAndByteReader) (err error) { +func (b *SideData) FromReader(reader readerAndByteReader, version ShareVersion) (err error) { var ( uncleCount uint64 uncleHash types.Hash @@ -54,9 +70,14 @@ func (b *SideData) FromReader(reader readerAndByteReader) (err error) { if _, err = io.ReadFull(reader, b.PublicViewKey[:]); err != nil { return err } - if _, err = io.ReadFull(reader, b.CoinbasePrivateKey[:]); err != nil { + if _, err = io.ReadFull(reader, b.CoinbasePrivateKeySeed[:]); err != nil { return err } + if version > ShareVersion_V1 { + //needs preprocessing + } else { + b.CoinbasePrivateKey = crypto.PrivateKeyBytes(b.CoinbasePrivateKeySeed) + } if _, err = io.ReadFull(reader, b.Parent[:]); err != nil { return err } @@ -95,11 +116,25 @@ func (b *SideData) FromReader(reader readerAndByteReader) (err error) { return err } } + if version > ShareVersion_V1 { + if err = binary.Read(reader, binary.LittleEndian, &b.ExtraBuffer.SoftwareId); err != nil { + return err + } + if err = binary.Read(reader, binary.LittleEndian, &b.ExtraBuffer.Version); err != nil { + return err + } + if err = binary.Read(reader, binary.LittleEndian, &b.ExtraBuffer.RandomNumber); err != nil { + return err + } + if err = binary.Read(reader, binary.LittleEndian, &b.ExtraBuffer.SideChainExtraNonce); err != nil { + return err + } + } return nil } -func (b *SideData) UnmarshalBinary(data []byte) error { +func (b *SideData) UnmarshalBinary(data []byte, version ShareVersion) error { reader := bytes.NewReader(data) - return b.FromReader(reader) + return b.FromReader(reader, version) } diff --git a/types/difficulty.go b/types/difficulty.go index b9db0a5..3d01843 100644 --- a/types/difficulty.go +++ b/types/difficulty.go @@ -15,6 +15,7 @@ import ( const DifficultySize = 16 var ZeroDifficulty = Difficulty(uint128.Zero) +var MaxDifficulty = Difficulty(uint128.Max) type Difficulty uint128.Uint128 diff --git a/utils/xorshift64star.go b/utils/xorshift64star.go new file mode 100644 index 0000000..9f94318 --- /dev/null +++ b/utils/xorshift64star.go @@ -0,0 +1,8 @@ +package utils + +func XorShift64Star(x uint64) uint64 { + x ^= x >> 12 + x ^= x << 25 + x ^= x >> 27 + return x * 0x2545F4914F6CDD1D +}