diff --git a/node/config/config.go b/node/config/config.go index b86f492..713cace 100644 --- a/node/config/config.go +++ b/node/config/config.go @@ -137,7 +137,7 @@ var unlock *SignedGenesisUnlock func DownloadAndVerifyGenesis(network uint) (*SignedGenesisUnlock, error) { if network != 0 { unlock = &SignedGenesisUnlock{ - GenesisSeedHex: "726573697374206d7563682c206f626579206c6974746c657c000000000000000000000007", + GenesisSeedHex: "726573697374206d7563682c206f626579206c6974746c657c000000000000000000000008", Beacon: []byte{ 0x58, 0xef, 0xd9, 0x7e, 0xdd, 0x0e, 0xb6, 0x2f, 0x51, 0xc7, 0x5d, 0x00, 0x29, 0x12, 0x45, 0x49, diff --git a/node/config/version.go b/node/config/version.go index 5576213..761a1dd 100644 --- a/node/config/version.go +++ b/node/config/version.go @@ -40,5 +40,5 @@ func GetPatchNumber() byte { } func GetRCNumber() byte { - return 0x01 + return 0x02 } diff --git a/node/consensus/data/consensus_frames.go b/node/consensus/data/consensus_frames.go index 0fe192e..b5466a8 100644 --- a/node/consensus/data/consensus_frames.go +++ b/node/consensus/data/consensus_frames.go @@ -90,7 +90,7 @@ func (e *DataClockConsensusEngine) prove( var validTransactions *protobufs.TokenRequests var invalidTransactions *protobufs.TokenRequests app, validTransactions, invalidTransactions, err = app.ApplyTransitions( - previousFrame.FrameNumber, + previousFrame.FrameNumber+1, e.stagedTransactions, true, ) diff --git a/node/consensus/data/data_clock_consensus_engine.go b/node/consensus/data/data_clock_consensus_engine.go index 323dde0..eed7d7b 100644 --- a/node/consensus/data/data_clock_consensus_engine.go +++ b/node/consensus/data/data_clock_consensus_engine.go @@ -18,6 +18,7 @@ import ( "github.com/multiformats/go-multiaddr" mn "github.com/multiformats/go-multiaddr/net" "github.com/pkg/errors" + mt "github.com/txaty/go-merkletree" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -98,6 +99,7 @@ type DataClockConsensusEngine struct { statsClient protobufs.NodeStatsClient currentReceivingSyncPeersMx sync.Mutex currentReceivingSyncPeers int + announcedJoin int beaconPeerId []byte frameChan chan *protobufs.ClockFrame @@ -544,6 +546,8 @@ func (e *DataClockConsensusEngine) Start() <-chan error { } } + var previousTree *mt.MerkleTree + for e.state < consensus.EngineStateStopping { nextFrame, err := e.dataTimeReel.Head() if err != nil { @@ -551,12 +555,17 @@ func (e *DataClockConsensusEngine) Start() <-chan error { } if frame.FrameNumber == nextFrame.FrameNumber { - time.Sleep(5 * time.Second) + time.Sleep(1 * time.Second) + continue + } + + if nextFrame.Timestamp < time.Now().UnixMilli()-30000 { + time.Sleep(1 * time.Second) continue } frame = nextFrame - _, tries, err := e.clockStore.GetDataClockFrame( + _, triesAtFrame, err := e.clockStore.GetDataClockFrame( e.filter, frame.FrameNumber, false, @@ -565,10 +574,40 @@ func (e *DataClockConsensusEngine) Start() <-chan error { panic(err) } - for i, trie := range tries[1:] { + modulo := len(clients) + + for i, trie := range triesAtFrame[1:] { if trie.Contains(peerProvingKeyAddress) { e.logger.Info("creating data shard ring proof", zap.Int("ring", i)) - e.PerformTimeProof(frame, frame.Difficulty, clients) + outputs := e.PerformTimeProof(frame, frame.Difficulty, clients) + proofTree, payload, output := tries.PackOutputIntoPayloadAndProof( + outputs, + modulo, + frame, + previousTree, + ) + previousTree = proofTree + + sig, err := e.pubSub.SignMessage( + payload, + ) + if err != nil { + panic(err) + } + + e.publishMessage(e.txFilter, &protobufs.TokenRequest{ + Request: &protobufs.TokenRequest_Mint{ + Mint: &protobufs.MintCoinRequest{ + Proofs: output, + Signature: &protobufs.Ed448Signature{ + PublicKey: &protobufs.Ed448PublicKey{ + KeyValue: e.pubSub.GetPublicKey(), + }, + Signature: sig, + }, + }, + }, + }) } } } @@ -581,10 +620,10 @@ func (e *DataClockConsensusEngine) PerformTimeProof( frame *protobufs.ClockFrame, difficulty uint32, clients []protobufs.DataIPCServiceClient, -) []byte { +) []mt.DataBlock { wg := sync.WaitGroup{} wg.Add(len(clients)) - output := make([][]byte, len(clients)) + output := make([]mt.DataBlock, len(clients)) for i, client := range clients { i := i client := client @@ -660,37 +699,18 @@ func (e *DataClockConsensusEngine) PerformTimeProof( continue } - output[i] = resp.Output + output[i] = tries.NewProofLeaf(resp.Output) break } + if output[i] == nil { + output[i] = tries.NewProofLeaf([]byte{}) + } wg.Done() }() } wg.Wait() - payload := []byte("mint") - for _, out := range output { - payload = append(payload, out...) - } - sig, err := e.pubSub.SignMessage( - payload, - ) - if err != nil { - panic(err) - } - e.publishMessage(e.txFilter, &protobufs.TokenRequest{ - Request: &protobufs.TokenRequest_Mint{ - Mint: &protobufs.MintCoinRequest{ - Proofs: output, - Signature: &protobufs.Ed448Signature{ - PublicKey: &protobufs.Ed448PublicKey{ - KeyValue: e.pubSub.GetPublicKey(), - }, - Signature: sig, - }, - }, - }, - }) - return []byte{} + + return output } func (e *DataClockConsensusEngine) Stop(force bool) <-chan error { diff --git a/node/consensus/data/main_data_loop.go b/node/consensus/data/main_data_loop.go index 379122e..c32f540 100644 --- a/node/consensus/data/main_data_loop.go +++ b/node/consensus/data/main_data_loop.go @@ -118,20 +118,9 @@ func (e *DataClockConsensusEngine) processFrame( return nextFrame } else { - _, tries, err := e.clockStore.GetDataClockFrame( - e.filter, - latestFrame.FrameNumber, - false, - ) - if err != nil { - e.logger.Error("error while fetching frame", zap.Error(err)) - return latestFrame - } - found := false - for _, trie := range tries[1:] { - found = found || trie.Contains(e.pubSub.GetPeerID()) - } - if !found && dataFrame.Timestamp > time.Now().UnixMilli()-30000 { + e.announcedJoin++ + if e.announcedJoin < 5 && !e.IsInProverTrie(e.pubSub.GetPeerID()) && + dataFrame.Timestamp > time.Now().UnixMilli()-30000 { e.logger.Info("announcing prover join") e.announceProverJoin() } diff --git a/node/execution/intrinsics/token/application/token_handle_mint.go b/node/execution/intrinsics/token/application/token_handle_mint.go index 633f13e..65a76ad 100644 --- a/node/execution/intrinsics/token/application/token_handle_mint.go +++ b/node/execution/intrinsics/token/application/token_handle_mint.go @@ -3,6 +3,7 @@ package application import ( "bytes" "encoding/binary" + "fmt" "math/big" "github.com/iden3/go-iden3-crypto/poseidon" @@ -15,6 +16,7 @@ import ( "source.quilibrium.com/quilibrium/monorepo/node/crypto" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" "source.quilibrium.com/quilibrium/monorepo/node/store" + "source.quilibrium.com/quilibrium/monorepo/node/tries" ) func (a *TokenApplication) handleMint( @@ -147,52 +149,94 @@ func (a *TokenApplication) handleMint( ) return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") } - challenge := []byte{} - challenge = append(challenge, peerId...) - challenge = binary.BigEndian.AppendUint64( - challenge, - currentFrameNumber-1, - ) - digest := make([]byte, 128) - s := sha3.NewShake256() - pubkey, _ := pk.Raw() - s.Write(pubkey) - _, err = s.Read(digest) + _, prfs, err := a.CoinStore.GetPreCoinProofsForOwner( + altAddr.FillBytes(make([]byte, 32)), + ) if err != nil { - panic(err) + return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") } - outputs := []*protobufs.TokenOutput{} - proofs := []byte{} - hits := 0 + var delete *protobufs.PreCoinProof + var commitment []byte + var previousFrame *protobufs.ClockFrame + for _, pr := range prfs { + if len(pr.Proof) >= 3 && len(pr.Commitment) == 40 { + delete = pr + commitment = pr.Commitment[:32] + previousFrameNumber := binary.BigEndian.Uint64(pr.Commitment[32:]) + previousFrame, _, err = a.ClockStore.GetDataClockFrame( + frame.Filter, + previousFrameNumber, + false, + ) - for i, p := range t.Proofs { + if err != nil { + a.Logger.Debug( + "invalid frame", + zap.Error(err), + zap.String("peer_id", base58.Encode([]byte(peerId))), + zap.Uint64("frame_number", currentFrameNumber), + ) + return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") + } + } + } + + newCommitment, parallelism, newFrame, verified, err := + tries.UnpackAndVerifyOutput(commitment, t.Proofs) + if err != nil { + a.Logger.Debug( + "mint error", + zap.Error(err), + zap.String("peer_id", base58.Encode([]byte(peerId))), + zap.Uint64("frame_number", currentFrameNumber), + ) + return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") + } + + if !verified { + a.Logger.Debug( + "tree verification failed", + zap.String("peer_id", base58.Encode([]byte(peerId))), + zap.Uint64("frame_number", currentFrameNumber), + ) + } + + if verified && delete != nil && len(t.Proofs) > 3 { + hash := sha3.Sum256(previousFrame.Output) + pick := tries.BytesToUnbiasedMod(hash, uint64(parallelism)) + challenge := []byte{} + challenge = append(challenge, peerId...) + challenge = binary.BigEndian.AppendUint64( + challenge, + previousFrame.FrameNumber, + ) individualChallenge := append([]byte{}, challenge...) individualChallenge = binary.BigEndian.AppendUint32( individualChallenge, - uint32(i), + uint32(pick), ) - individualChallenge = append(individualChallenge, frame.Output...) - if len(p) != 516 { + leaf := t.Proofs[len(t.Proofs)-1] + individualChallenge = append(individualChallenge, previousFrame.Output...) + if len(leaf) != 516 { a.Logger.Debug( "invalid size", zap.String("peer_id", base58.Encode([]byte(peerId))), zap.Uint64("frame_number", currentFrameNumber), - zap.Int("proof_size", len(p)), + zap.Int("proof_size", len(leaf)), ) - continue + return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") } - hits++ - wesoProver := crypto.NewWesolowskiFrameProver(a.Logger) - - if !wesoProver.VerifyChallengeProof( - individualChallenge, - frame.Difficulty, - p, - ) { + fmt.Printf("%x\n", individualChallenge) + if bytes.Equal(leaf, bytes.Repeat([]byte{0x00}, 516)) || + !wesoProver.VerifyChallengeProof( + individualChallenge, + frame.Difficulty, + leaf, + ) { a.Logger.Debug( "invalid proof", zap.String("peer_id", base58.Encode([]byte(peerId))), @@ -200,71 +244,104 @@ func (a *TokenApplication) handleMint( ) return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") } - - proofs = append(proofs, p...) } - if hits == 0 { + outputs := []*protobufs.TokenOutput{} + + if delete != nil { + outputs = append( + outputs, + &protobufs.TokenOutput{ + Output: &protobufs.TokenOutput_DeletedProof{ + DeletedProof: delete, + }, + }, + ) + } + if verified && delete != nil && len(t.Proofs) > 3 { + + ringFactor := big.NewInt(2) + ringFactor.Exp(ringFactor, big.NewInt(int64(ring)), nil) + + // const for testnet + storage := big.NewInt(int64(256 * parallelism)) + unitFactor := big.NewInt(8000000000) + storage.Mul(storage, unitFactor) + storage.Quo(storage, big.NewInt(proverSet)) + storage.Quo(storage, ringFactor) + a.Logger.Debug( - "no proofs", + "issued reward", zap.String("peer_id", base58.Encode([]byte(peerId))), zap.Uint64("frame_number", currentFrameNumber), + zap.String("reward", storage.String()), + ) + + outputs = append( + outputs, + &protobufs.TokenOutput{ + Output: &protobufs.TokenOutput_Proof{ + Proof: &protobufs.PreCoinProof{ + Commitment: binary.BigEndian.AppendUint64( + append([]byte{}, newCommitment...), + newFrame, + ), + Amount: storage.FillBytes(make([]byte, 32)), + Proof: payload, + Difficulty: a.Difficulty, + Owner: &protobufs.AccountRef{ + Account: &protobufs.AccountRef_ImplicitAccount{ + ImplicitAccount: &protobufs.ImplicitAccount{ + ImplicitType: 0, + Address: altAddr.FillBytes(make([]byte, 32)), + }, + }, + }, + }, + }, + }, + &protobufs.TokenOutput{ + Output: &protobufs.TokenOutput_Coin{ + Coin: &protobufs.Coin{ + Amount: storage.FillBytes(make([]byte, 32)), + Intersection: make([]byte, 1024), + Owner: &protobufs.AccountRef{ + Account: &protobufs.AccountRef_ImplicitAccount{ + ImplicitAccount: &protobufs.ImplicitAccount{ + ImplicitType: 0, + Address: altAddr.FillBytes(make([]byte, 32)), + }, + }, + }, + }, + }, + }, + ) + } else { + outputs = append( + outputs, + &protobufs.TokenOutput{ + Output: &protobufs.TokenOutput_Proof{ + Proof: &protobufs.PreCoinProof{ + Commitment: binary.BigEndian.AppendUint64( + append([]byte{}, newCommitment...), + newFrame, + ), + Proof: payload, + Difficulty: a.Difficulty, + Owner: &protobufs.AccountRef{ + Account: &protobufs.AccountRef_ImplicitAccount{ + ImplicitAccount: &protobufs.ImplicitAccount{ + ImplicitType: 0, + Address: altAddr.FillBytes(make([]byte, 32)), + }, + }, + }, + }, + }, + }, ) - return nil, errors.Wrap(ErrInvalidStateTransition, "handle mint") } - - ringFactor := big.NewInt(2) - ringFactor.Exp(ringFactor, big.NewInt(int64(ring)), nil) - - storage := big.NewInt(int64(512 * hits)) - unitFactor := big.NewInt(8000000000) - storage.Mul(storage, unitFactor) - storage.Quo(storage, big.NewInt(proverSet)) - storage.Quo(storage, ringFactor) - - a.Logger.Debug( - "issued reward", - zap.String("peer_id", base58.Encode([]byte(peerId))), - zap.Uint64("frame_number", currentFrameNumber), - zap.String("reward", storage.String()), - ) - - outputs = append( - outputs, - &protobufs.TokenOutput{ - Output: &protobufs.TokenOutput_Proof{ - Proof: &protobufs.PreCoinProof{ - Amount: storage.FillBytes(make([]byte, 32)), - Proof: proofs, - Difficulty: a.Difficulty, - Owner: &protobufs.AccountRef{ - Account: &protobufs.AccountRef_ImplicitAccount{ - ImplicitAccount: &protobufs.ImplicitAccount{ - ImplicitType: 0, - Address: addr.FillBytes(make([]byte, 32)), - }, - }, - }, - }, - }, - }, - &protobufs.TokenOutput{ - Output: &protobufs.TokenOutput_Coin{ - Coin: &protobufs.Coin{ - Amount: storage.FillBytes(make([]byte, 32)), - Intersection: make([]byte, 1024), - Owner: &protobufs.AccountRef{ - Account: &protobufs.AccountRef_ImplicitAccount{ - ImplicitAccount: &protobufs.ImplicitAccount{ - ImplicitType: 0, - Address: addr.FillBytes(make([]byte, 32)), - }, - }, - }, - }, - }, - }, - ) lockMap[string(t.Signature.PublicKey.KeyValue)] = struct{}{} return outputs, nil } diff --git a/node/execution/intrinsics/token/application/token_handle_prover_join_test.go b/node/execution/intrinsics/token/application/token_handle_prover_join_test.go index 65d4697..3ee4ad5 100644 --- a/node/execution/intrinsics/token/application/token_handle_prover_join_test.go +++ b/node/execution/intrinsics/token/application/token_handle_prover_join_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/binary" "encoding/hex" + "fmt" "testing" "time" @@ -13,8 +14,10 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/assert" + "github.com/txaty/go-merkletree" "go.uber.org/zap" qcrypto "source.quilibrium.com/quilibrium/monorepo/node/crypto" + "source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/token" "source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/token/application" "source.quilibrium.com/quilibrium/monorepo/node/p2p" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" @@ -144,39 +147,45 @@ func TestHandleProverJoin(t *testing.T) { assert.Error(t, err) txn, _ = app.ClockStore.NewTransaction() frame2, _ := wprover.ProveDataClockFrame(frame1, [][]byte{}, []*protobufs.InclusionAggregateProof{}, bprivKey, time.Now().UnixMilli(), 10000) - selbi, _ = frame1.GetSelector() - app.ClockStore.StageDataClockFrame(selbi.FillBytes(make([]byte, 32)), frame1, txn) - app.ClockStore.CommitDataClockFrame(frame2.Filter, 1, selbi.FillBytes(make([]byte, 32)), app.Tries, txn, false) + selbi, _ = frame2.GetSelector() + app.ClockStore.StageDataClockFrame(selbi.FillBytes(make([]byte, 32)), frame2, txn) + app.ClockStore.CommitDataClockFrame(frame2.Filter, 2, selbi.FillBytes(make([]byte, 32)), app.Tries, txn, false) txn.Commit() challenge := []byte{} challenge = append(challenge, []byte(peerId)...) challenge = binary.BigEndian.AppendUint64( challenge, - 1, + 2, ) individualChallenge := append([]byte{}, challenge...) individualChallenge = binary.BigEndian.AppendUint32( individualChallenge, uint32(0), ) - individualChallenge = append(individualChallenge, frame1.Output...) + individualChallenge = append(individualChallenge, frame2.Output...) + fmt.Printf("%x\n", individualChallenge) out, _ := wprover.CalculateChallengeProof(individualChallenge, 10000) - payload = []byte("mint") - payload = append(payload, out...) + proofTree, payload, output := tries.PackOutputIntoPayloadAndProof( + []merkletree.DataBlock{tries.NewProofLeaf(out), tries.NewProofLeaf(make([]byte, 516))}, + 2, + frame2, + nil, + ) + sig, _ = privKey.Sign(payload) - _, _, _, err = app.ApplyTransitions(2, &protobufs.TokenRequests{ + app, success, _, err = app.ApplyTransitions(2, &protobufs.TokenRequests{ Requests: []*protobufs.TokenRequest{ &protobufs.TokenRequest{ Request: &protobufs.TokenRequest_Mint{ Mint: &protobufs.MintCoinRequest{ - Proofs: [][]byte{out}, + Proofs: output, Signature: &protobufs.Ed448Signature{ - Signature: sig, PublicKey: &protobufs.Ed448PublicKey{ KeyValue: pubkey, }, + Signature: sig, }, }, }, @@ -185,4 +194,69 @@ func TestHandleProverJoin(t *testing.T) { }, false) assert.NoError(t, err) + assert.Len(t, success.Requests, 1) + assert.Len(t, app.TokenOutputs.Outputs, 1) + txn, _ = app.CoinStore.NewTransaction() + for i, o := range app.TokenOutputs.Outputs { + switch e := o.Output.(type) { + case *protobufs.TokenOutput_Coin: + a, err := token.GetAddressOfCoin(e.Coin, 1, uint64(i)) + assert.NoError(t, err) + err = app.CoinStore.PutCoin(txn, 1, a, e.Coin) + assert.NoError(t, err) + case *protobufs.TokenOutput_DeletedCoin: + c, err := app.CoinStore.GetCoinByAddress(txn, e.DeletedCoin.Address) + assert.NoError(t, err) + err = app.CoinStore.DeleteCoin(txn, e.DeletedCoin.Address, c) + assert.NoError(t, err) + case *protobufs.TokenOutput_Proof: + a, err := token.GetAddressOfPreCoinProof(e.Proof) + assert.NoError(t, err) + err = app.CoinStore.PutPreCoinProof(txn, 1, a, e.Proof) + assert.NoError(t, err) + case *protobufs.TokenOutput_DeletedProof: + a, err := token.GetAddressOfPreCoinProof(e.DeletedProof) + assert.NoError(t, err) + c, err := app.CoinStore.GetPreCoinProofByAddress(a) + assert.NoError(t, err) + err = app.CoinStore.DeletePreCoinProof(txn, a, c) + assert.NoError(t, err) + } + } + err = txn.Commit() + txn, _ = app.ClockStore.NewTransaction() + frame3, _ := wprover.ProveDataClockFrame(frame2, [][]byte{}, []*protobufs.InclusionAggregateProof{}, bprivKey, time.Now().UnixMilli(), 10000) + selbi, _ = frame3.GetSelector() + app.ClockStore.StageDataClockFrame(selbi.FillBytes(make([]byte, 32)), frame3, txn) + app.ClockStore.CommitDataClockFrame(frame3.Filter, 1, selbi.FillBytes(make([]byte, 32)), app.Tries, txn, false) + txn.Commit() + + proofTree, payload, output = tries.PackOutputIntoPayloadAndProof( + []merkletree.DataBlock{tries.NewProofLeaf(out), tries.NewProofLeaf(make([]byte, 516))}, + 2, + frame3, + proofTree, + ) + + sig, _ = privKey.Sign(payload) + app, success, _, err = app.ApplyTransitions(3, &protobufs.TokenRequests{ + Requests: []*protobufs.TokenRequest{ + &protobufs.TokenRequest{ + Request: &protobufs.TokenRequest_Mint{ + Mint: &protobufs.MintCoinRequest{ + Proofs: output, + Signature: &protobufs.Ed448Signature{ + PublicKey: &protobufs.Ed448PublicKey{ + KeyValue: pubkey, + }, + Signature: sig, + }, + }, + }, + }, + }, + }, false) + assert.NoError(t, err) + assert.Len(t, success.Requests, 1) + assert.Len(t, app.TokenOutputs.Outputs, 3) } diff --git a/node/execution/intrinsics/token/token_execution_engine.go b/node/execution/intrinsics/token/token_execution_engine.go index b55ed59..d246f84 100644 --- a/node/execution/intrinsics/token/token_execution_engine.go +++ b/node/execution/intrinsics/token/token_execution_engine.go @@ -758,7 +758,7 @@ func (e *TokenExecutionEngine) VerifyExecution( } a, _, _, err = a.ApplyTransitions( - parent.FrameNumber, + frame.FrameNumber, transition, false, ) diff --git a/node/go.mod b/node/go.mod index 38e72e8..8880148 100644 --- a/node/go.mod +++ b/node/go.mod @@ -62,6 +62,7 @@ require ( github.com/pion/turn/v2 v2.1.6 // indirect github.com/pion/webrtc/v3 v3.2.40 // indirect github.com/rychipman/easylex v0.0.0-20160129204217-49ee7767142f // indirect + github.com/txaty/go-merkletree v0.2.2 // indirect go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect diff --git a/node/go.sum b/node/go.sum index 0cd058d..35bacff 100644 --- a/node/go.sum +++ b/node/go.sum @@ -520,6 +520,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/txaty/go-merkletree v0.2.2 h1:K5bHDFK+Q3KK+gEJeyTOECKuIwl/LVo4CI+cm0/p34g= +github.com/txaty/go-merkletree v0.2.2/go.mod h1:w5HPEu7ubNw5LzS+91m+1/GtuZcWHKiPU3vEGi+ThJM= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= diff --git a/node/tries/proof_leaf.go b/node/tries/proof_leaf.go new file mode 100644 index 0000000..96823e2 --- /dev/null +++ b/node/tries/proof_leaf.go @@ -0,0 +1,176 @@ +package tries + +import ( + "encoding/binary" + "fmt" + "math" + "math/bits" + + "github.com/pkg/errors" + mt "github.com/txaty/go-merkletree" + "golang.org/x/crypto/sha3" + "source.quilibrium.com/quilibrium/monorepo/node/protobufs" +) + +type ProofLeaf struct { + output []byte +} + +var _ mt.DataBlock = (*ProofLeaf)(nil) + +func NewProofLeaf(output []byte) *ProofLeaf { + return &ProofLeaf{output} +} + +func (p *ProofLeaf) Serialize() ([]byte, error) { + return p.output, nil +} + +func PackOutputIntoPayloadAndProof( + outputs []mt.DataBlock, + modulo int, + frame *protobufs.ClockFrame, + previousTree *mt.MerkleTree, +) (*mt.MerkleTree, []byte, [][]byte) { + tree, err := mt.New( + &mt.Config{ + HashFunc: func(data []byte) ([]byte, error) { + hash := sha3.Sum256(data) + return hash[:], nil + }, + Mode: mt.ModeProofGen, + DisableLeafHashing: true, + }, + outputs, + ) + if err != nil { + panic(err) + } + + payload := []byte("mint") + payload = append(payload, tree.Root...) + payload = binary.BigEndian.AppendUint32(payload, uint32(modulo)) + payload = binary.BigEndian.AppendUint64(payload, frame.FrameNumber) + + output := [][]byte{ + tree.Root, + binary.BigEndian.AppendUint32([]byte{}, uint32(modulo)), + binary.BigEndian.AppendUint64([]byte{}, frame.FrameNumber), + } + + if previousTree != nil { + hash := sha3.Sum256(frame.Output) + pick := BytesToUnbiasedMod(hash, uint64(modulo)) + for _, sib := range previousTree.Proofs[int(pick)].Siblings { + payload = append(payload, sib...) + output = append(output, sib) + } + payload = binary.BigEndian.AppendUint32( + payload, + previousTree.Proofs[int(pick)].Path, + ) + output = append( + output, + binary.BigEndian.AppendUint32( + []byte{}, + previousTree.Proofs[int(pick)].Path, + ), + ) + payload = append(payload, previousTree.Leaves[int(pick)]...) + output = append(output, previousTree.Leaves[int(pick)]) + } + return tree, payload, output +} + +func UnpackAndVerifyOutput( + previousRoot []byte, + output [][]byte, +) (treeRoot []byte, modulo uint32, frameNumber uint64, verified bool, err error) { + if len(output) < 3 { + return nil, 0, 0, false, errors.Wrap( + fmt.Errorf("output too short, expected at least 3 elements"), + "unpack and verify output", + ) + } + + treeRoot = output[0] + modulo = binary.BigEndian.Uint32(output[1]) + frameNumber = binary.BigEndian.Uint64(output[2]) + + payload := []byte("mint") + payload = append(payload, treeRoot...) + payload = binary.BigEndian.AppendUint32(payload, modulo) + payload = binary.BigEndian.AppendUint64(payload, frameNumber) + + if len(output) > 3 { + numSiblings := bits.Len64(uint64(modulo) - 1) + if len(output) != 5+numSiblings { + return nil, 0, 0, false, errors.Wrap( + fmt.Errorf("invalid number of proof elements"), + "unpack and verify output", + ) + } + + siblings := output[3 : 3+numSiblings] + for _, sib := range siblings { + payload = append(payload, sib...) + } + + pathBytes := output[3+numSiblings] + path := binary.BigEndian.Uint32(pathBytes) + payload = binary.BigEndian.AppendUint32(payload, path) + + leaf := output[len(output)-1] + payload = append(payload, leaf...) + + verified, err = mt.Verify( + NewProofLeaf(leaf), + &mt.Proof{ + Siblings: siblings, + Path: path, + }, + previousRoot, + &mt.Config{ + HashFunc: func(data []byte) ([]byte, error) { + hash := sha3.Sum256(data) + return hash[:], nil + }, + Mode: mt.ModeProofGen, + DisableLeafHashing: true, + }, + ) + if err != nil { + return nil, 0, 0, false, errors.Wrap(err, "unpack and verify output") + } + } else { + verified = true + } + + return treeRoot, modulo, frameNumber, verified, nil +} + +func BytesToUnbiasedMod(input [32]byte, modulus uint64) uint64 { + if modulus <= 1 { + return 0 + } + + hashValue := binary.BigEndian.Uint64(input[:8]) + + maxValid := math.MaxUint64 - (math.MaxUint64 % modulus) + + result := hashValue + for result > maxValid { + offset := uint64(8) + for result > maxValid && offset <= 24 { + nextBytes := binary.BigEndian.Uint64(input[offset : offset+8]) + result = (result * 31) ^ nextBytes + offset += 8 + } + + if result > maxValid { + result = (result * 31) ^ (result >> 32) + } + } + + return result % modulus +} diff --git a/node/tries/proof_leaf_test.go b/node/tries/proof_leaf_test.go new file mode 100644 index 0000000..378391b --- /dev/null +++ b/node/tries/proof_leaf_test.go @@ -0,0 +1,173 @@ +package tries_test + +import ( + "crypto/rand" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + mt "github.com/txaty/go-merkletree" + "golang.org/x/crypto/sha3" + "source.quilibrium.com/quilibrium/monorepo/node/protobufs" + "source.quilibrium.com/quilibrium/monorepo/node/tries" +) + +func TestPackAndVerifyOutput(t *testing.T) { + testCases := []struct { + name string + numLeaves int + modulo int + frameNum uint64 + withPrev bool + }{ + { + name: "Basic case without previous tree", + numLeaves: 4, + modulo: 4, + frameNum: 1, + withPrev: false, + }, + { + name: "With previous tree", + numLeaves: 8, + modulo: 8, + frameNum: 2, + withPrev: true, + }, + { + name: "Large tree with previous", + numLeaves: 16, + modulo: 16, + frameNum: 3, + withPrev: true, + }, + { + name: "Non-power-of-2 modulo", + numLeaves: 10, + modulo: 7, + frameNum: 4, + withPrev: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + outputs := make([]mt.DataBlock, tc.numLeaves) + for i := range outputs { + data := make([]byte, 32) + binary.BigEndian.PutUint32(data, uint32(i)) + outputs[i] = tries.NewProofLeaf(data) + } + + frame := &protobufs.ClockFrame{ + FrameNumber: tc.frameNum, + Output: make([]byte, 516), + } + rand.Read(frame.Output) + + var previousTree *mt.MerkleTree + if tc.withPrev { + prevOutputs := make([]mt.DataBlock, tc.modulo) + for i := range prevOutputs { + data := make([]byte, 32) + binary.BigEndian.PutUint32(data, uint32(i)) + prevOutputs[i] = tries.NewProofLeaf(data) + } + + var err error + previousTree, err = mt.New( + &mt.Config{ + HashFunc: func(data []byte) ([]byte, error) { + hash := sha3.Sum256(data) + return hash[:], nil + }, + Mode: mt.ModeProofGen, + DisableLeafHashing: true, + }, + prevOutputs, + ) + require.NoError(t, err) + } + + tree, payload, output := tries.PackOutputIntoPayloadAndProof( + outputs, + tc.modulo, + frame, + previousTree, + ) + require.NotNil(t, tree) + require.NotEmpty(t, payload) + require.NotEmpty(t, output) + + var previousRoot []byte + if previousTree != nil { + previousRoot = previousTree.Root + } + + treeRoot, modulo, frameNumber, verified, err := tries.UnpackAndVerifyOutput( + previousRoot, + output, + ) + + require.NoError(t, err) + require.True(t, verified, "Output verification failed") + require.Equal(t, tree.Root, treeRoot, "Tree root mismatch") + require.Equal(t, uint32(tc.modulo), modulo, "Modulo mismatch") + require.Equal(t, tc.frameNum, frameNumber, "Frame number mismatch") + + reconstructedPayload := []byte("mint") + reconstructedPayload = append(reconstructedPayload, treeRoot...) + reconstructedPayload = binary.BigEndian.AppendUint32(reconstructedPayload, modulo) + reconstructedPayload = binary.BigEndian.AppendUint64(reconstructedPayload, frameNumber) + + if tc.withPrev { + for i := 3; i < len(output)-2; i++ { + reconstructedPayload = append(reconstructedPayload, output[i]...) + } + pathBytes := output[len(output)-2] + reconstructedPayload = append(reconstructedPayload, pathBytes...) + leafBytes := output[len(output)-1] + reconstructedPayload = append(reconstructedPayload, leafBytes...) + } + + require.Equal(t, payload, reconstructedPayload, "Payload reconstruction mismatch") + + if tc.withPrev { + t.Run("corrupted_proof", func(t *testing.T) { + corruptedOutput := make([][]byte, len(output)) + copy(corruptedOutput, output) + if len(corruptedOutput) > 3 { + corruptedSibling := make([]byte, len(corruptedOutput[3])) + copy(corruptedSibling, corruptedOutput[3]) + corruptedSibling[0] ^= 0xFF + corruptedOutput[3] = corruptedSibling + } + + _, _, _, verified, err := tries.UnpackAndVerifyOutput( + previousRoot, + corruptedOutput, + ) + require.False(t, verified, "Verification should fail with corrupted sibling") + require.NoError(t, err, "Unexpected error with corrupted sibling") + + corruptedOutput = make([][]byte, len(output)) + copy(corruptedOutput, output) + if len(corruptedOutput) > 0 { + lastIdx := len(corruptedOutput) - 1 + corruptedLeaf := make([]byte, len(corruptedOutput[lastIdx])) + copy(corruptedLeaf, corruptedOutput[lastIdx]) + corruptedLeaf[0] ^= 0xFF + corruptedOutput[lastIdx] = corruptedLeaf + } + + _, _, _, verified, err = tries.UnpackAndVerifyOutput( + previousRoot, + corruptedOutput, + ) + require.False(t, verified, "Verification should fail with corrupted leaf") + require.NoError(t, err, "Unexpected error with corrupted leaf") + }) + } + }) + } +}