--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java
+++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java
@@ -174,6 +_,12 @@
     @Override
     protected boolean triggerEvent(BlockState state, Level level, BlockPos pos, int id, int param) {
         Direction direction = state.getValue(FACING);
+        // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; prevent retracting when we're facing the wrong way (we were replaced before retraction could occur)
+        Direction directionQueuedAs = Direction.from3DDataValue(param & 7); // Paper - copied from below
+        if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits && direction != directionQueuedAs) {
+            return false;
+        }
+        // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
         BlockState blockState = state.setValue(EXTENDED, Boolean.valueOf(true));
         if (!level.isClientSide) {
             boolean neighborSignal = this.getNeighborSignal(level, pos, direction);
@@ -205,10 +_,17 @@
                 .defaultBlockState()
                 .setValue(MovingPistonBlock.FACING, direction)
                 .setValue(MovingPistonBlock.TYPE, this.isSticky ? PistonType.STICKY : PistonType.DEFAULT);
+            // Paper start - Fix sticky pistons and BlockPistonRetractEvent; Move empty piston retract call to fix multiple event fires
+            if (!this.isSticky) {
+                if (!new org.bukkit.event.block.BlockPistonRetractEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), java.util.Collections.emptyList(), org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction)).callEvent()) {
+                    return false;
+                }
+            }
+            // Paper end - Fix sticky pistons and BlockPistonRetractEvent
             level.setBlock(pos, blockState1, 20);
             level.setBlockEntity(
                 MovingPistonBlock.newMovingBlockEntity(
-                    pos, blockState1, this.defaultBlockState().setValue(FACING, Direction.from3DDataValue(param & 7)), direction, false, true
+                    pos, blockState1, this.defaultBlockState().setValue(FACING, Direction.from3DDataValue(param & 7)), direction, false, true // Paper - Protect Bedrock and End Portal/Frames from being destroyed; diff on change
                 )
             );
             level.blockUpdated(pos, blockState1.getBlock());
@@ -232,13 +_,27 @@
                         || blockState2.getPistonPushReaction() != PushReaction.NORMAL
                             && !blockState2.is(Blocks.PISTON)
                             && !blockState2.is(Blocks.STICKY_PISTON)) {
+                        // Paper start - Fix sticky pistons and BlockPistonRetractEvent; fire BlockPistonRetractEvent for sticky pistons retracting nothing (air)
+                        if (id == TRIGGER_CONTRACT && blockState1.isAir()) {
+                            if (!new org.bukkit.event.block.BlockPistonRetractEvent(org.bukkit.craftbukkit.block.CraftBlock.at(level, pos), java.util.Collections.emptyList(), org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction)).callEvent()) {
+                                return false;
+                            }
+                        }
+                        // Paper end - Fix sticky pistons and BlockPistonRetractEvent
                         level.removeBlock(pos.relative(direction), false);
                     } else {
                         this.moveBlocks(level, pos, direction, false);
                     }
                 }
             } else {
-                level.removeBlock(pos.relative(direction), false);
+                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed; fix headless pistons breaking blocks
+                BlockPos headPos = pos.relative(direction);
+                if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits || level.getBlockState(headPos) == Blocks.PISTON_HEAD.defaultBlockState().setValue(FACING, direction)) { // double check to make sure we're not a headless piston.
+                    level.removeBlock(headPos, false);
+                } else {
+                    ((ServerLevel) level).getChunkSource().blockChanged(headPos); // ... fix client desync
+                }
+                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
             }
 
             level.playSound(null, pos, SoundEvents.PISTON_CONTRACT, SoundSource.BLOCKS, 0.5F, level.random.nextFloat() * 0.15F + 0.6F);
@@ -249,7 +_,7 @@
     }
 
     public static boolean isPushable(BlockState state, Level level, BlockPos pos, Direction movementDirection, boolean allowDestroy, Direction pistonFacing) {
-        if (pos.getY() < level.getMinY() || pos.getY() > level.getMaxY() || !level.getWorldBorder().isWithinBounds(pos)) {
+        if (pos.getY() < level.getMinY() || pos.getY() > level.getMaxY() || !level.getWorldBorder().isWithinBounds(pos) || !level.getWorldBorder().isWithinBounds(pos.relative(movementDirection))) { // Paper - Fix piston world border check
             return false;
         } else if (state.isAir()) {
             return true;
@@ -305,12 +_,54 @@
             BlockState[] blockStates = new BlockState[toPush.size() + toDestroy.size()];
             Direction direction = extending ? facing : facing.getOpposite();
             int i = 0;
+            // CraftBukkit start
+            final org.bukkit.block.Block bblock = level.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
+
+            final List<BlockPos> moved = pistonStructureResolver.getToPush();
+            final List<BlockPos> broken = pistonStructureResolver.getToDestroy();
+
+            List<org.bukkit.block.Block> blocks = new java.util.AbstractList<>() {
+
+                @Override
+                public int size() {
+                    return moved.size() + broken.size();
+                }
+
+                @Override
+                public org.bukkit.block.Block get(int index) {
+                    if (index >= this.size() || index < 0) {
+                        throw new ArrayIndexOutOfBoundsException(index);
+                    }
+                    BlockPos pos = index < moved.size() ? moved.get(index) : broken.get(index - moved.size());
+                    return bblock.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
+                }
+            };
+            org.bukkit.event.block.BlockPistonEvent event;
+            if (extending) {
+                event = new org.bukkit.event.block.BlockPistonExtendEvent(bblock, blocks, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction));
+            } else {
+                event = new org.bukkit.event.block.BlockPistonRetractEvent(bblock, blocks, org.bukkit.craftbukkit.block.CraftBlock.notchToBlockFace(direction));
+            }
+            level.getCraftServer().getPluginManager().callEvent(event);
+
+            if (event.isCancelled()) {
+                for (BlockPos b : broken) {
+                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
+                }
+                for (BlockPos b : moved) {
+                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
+                    b = b.relative(direction);
+                    level.sendBlockUpdated(b, Blocks.AIR.defaultBlockState(), level.getBlockState(b), 3);
+                }
+                return false;
+            }
+            // CraftBukkit end
 
             for (int i1 = toDestroy.size() - 1; i1 >= 0; i1--) {
                 BlockPos blockPos2 = toDestroy.get(i1);
                 BlockState blockState1 = level.getBlockState(blockPos2);
                 BlockEntity blockEntity = blockState1.hasBlockEntity() ? level.getBlockEntity(blockPos2) : null;
-                dropResources(blockState1, level, blockPos2, blockEntity);
+                dropResources(blockState1, level, blockPos2, blockEntity, pos); // Paper - Add BlockBreakBlockEvent
                 level.setBlock(blockPos2, Blocks.AIR.defaultBlockState(), 18);
                 level.gameEvent(GameEvent.BLOCK_DESTROY, blockPos2, GameEvent.Context.of(blockState1));
                 if (!blockState1.is(BlockTags.FIRE)) {
@@ -321,13 +_,26 @@
             }
 
             for (int i1 = toPush.size() - 1; i1 >= 0; i1--) {
-                BlockPos blockPos2 = toPush.get(i1);
-                BlockState blockState1 = level.getBlockState(blockPos2);
+                // Paper start - fix a variety of piston desync dupes
+                boolean allowDesync = io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication;
+                BlockPos blockPos2;
+                BlockPos oldPos = blockPos2 = toPush.get(i1);
+                BlockState blockState1 = allowDesync ? level.getBlockState(oldPos) : null;
+                // Paper end - fix a variety of piston desync dupes
                 blockPos2 = blockPos2.relative(direction);
                 map.remove(blockPos2);
                 BlockState blockState2 = Blocks.MOVING_PISTON.defaultBlockState().setValue(FACING, facing);
                 level.setBlock(blockPos2, blockState2, 68);
-                level.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockPos2, blockState2, list.get(i1), facing, extending, false));
+                // Paper start - fix a variety of piston desync dupes
+                if (!allowDesync) {
+                    blockState1 = level.getBlockState(oldPos);
+                    map.replace(oldPos, blockState1);
+                }
+                level.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(blockPos2, blockState2, allowDesync ? list.get(i1) : blockState1, facing, extending, false));
+                if (!allowDesync) {
+                    level.setBlock(oldPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_MOVE_BY_PISTON | 1024); // set air to prevent later physics updates from seeing this block
+                }
+                // Paper end - fix a variety of piston desync dupes
                 blockStates[i++] = blockState1;
             }
 
